106

When you do Something.find(array_of_ids) in Rails, the order of the resulting array does not depend on the order of array_of_ids.

Is there any way to do the find and preserve the order?

ATM I manually sort the records based on order of IDs, but that is kind of lame.

UPD: if it's possible to specify the order using the :order param and some kind of SQL clause, then how?

Mike Woodhouse
  • 51,832
  • 12
  • 88
  • 127
Leonid Shevtsov
  • 14,024
  • 9
  • 51
  • 82
  • 4
    possible duplicate of [Clean way to find ActiveRecord objects by id in the order specified](http://stackoverflow.com/questions/801824/clean-way-to-find-activerecord-objects-by-id-in-the-order-specified) – mu is too short Mar 22 '13 at 06:56
  • for me Postgres 10.5, Ruby 2.7 and RoR 6.0.3.7: this solution worked: https://stackoverflow.com/a/33975169/2091508 – emkays Aug 23 '21 at 16:30

15 Answers15

80

Oddly, no one has suggested something like this:

index = Something.find(array_of_ids).group_by(&:id)
array_of_ids.map { |i| index[i].first }

As efficient as it gets besides letting SQL backend do it.

Edit: To improve on my own answer, you can also do it like this:

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

#index_by and #slice are pretty handy additions in ActiveSupport for arrays and hashes respectively.

Gunchars
  • 9,555
  • 3
  • 28
  • 27
  • So your edit seems to work but it makes me nervous key order in a hash isn't guaranteed is it? so when you call slice and get the hash back "re-ordered" it's really depending on the hash returning values in the order that it's keys were added. This feels like depending on an implementation detail that may change. – Jon Sep 03 '13 at 17:45
  • 2
    @Jon, the order is guaranteed in Ruby 1.9 and every other implementation that tries to follow it. For 1.8, Rails (ActiveSupport) patches the Hash class to make it behave the same way, so if you're using Rails, you should be ok. – Gunchars Sep 03 '13 at 21:39
  • thanks for the clarification, just found that in the documentation. – Jon Sep 04 '13 at 04:46
  • I really like this solution (index_by-slice-values) ... cool stuff :-) – hurikhan77 Apr 14 '14 at 23:36
  • 18
    The problem with this is that it returns an array, rather than a relation. – Velizar Hristov Mar 17 '15 at 01:52
  • 3
    Great, however, the one-liner does not work for me (Rails 4.1) – Besi Apr 04 '15 at 16:17
  • The accepted answer does return an active record relation. – aaron-coding Oct 26 '15 at 20:03
  • @aaron-coding sadly, the accepted answer only works with MySQL though, so it depends on your use-case. @Gunchars - does `index_by` already sort the keys in the hash? (and then `slice` might not be needed?) – gingerlime Nov 11 '15 at 10:48
  • I made a slight change to @Gunchars improved result -- I used `sorted = Something.find(array_of_ids).index_by(&:id).extract!(*array_of_ids).values` because slice did not seem to express the ordering function, even though it works correctly. – tomf Jun 28 '16 at 20:10
  • loved #index_by! – brauliobo Jun 27 '17 at 10:06
  • I benchmarked this approach vs offloading the sorting to DB )9https://www.justinweiss.com/articles/how-to-select-database-records-in-an-arbitrary-order//), and found that the DB approach has shortest time taken. – Martin Verdejo Sep 18 '18 at 07:00
72

The answer is for mysql only

There is a function in mysql called FIELD()

Here is how you could use it in .find():

>> ids = [100, 1, 6]
=> [100, 1, 6]

>> WordDocument.find(ids).collect(&:id)
=> [1, 6, 100]

>> WordDocument.find(ids, :order => "field(id, #{ids.join(',')})")
=> [100, 1, 6]

For new Version
>> WordDocument.where(id: ids).order("field(id, #{ids.join ','})")

Update: This will be removed in Rails 6.1 Rails source code

Abel
  • 3,989
  • 32
  • 31
kovyrin
  • 952
  • 6
  • 8
  • Do you happen to know the equivalent of `FIELDS()` in Postgres? – Trung Lê Jun 26 '12 at 06:27
  • 3
    I wrote a plpgsql function to do this in postgres - http://omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql – Omar Qureshi Jun 29 '12 at 16:58
  • 25
    This no longer works. For more recent Rails: `Object.where(id: ids).order("field(id, #{ids.join ','})")` – mahemoff Apr 18 '15 at 08:38
  • 3
    This is a better solution than Gunchars' because it won't break pagination. – pguardiario Jun 28 '15 at 02:11
  • .ids works fine for me, and is pretty fast [activerecord documentation](http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-ids) – TheJKFever Jul 08 '15 at 15:26
  • https://www.justinweiss.com/articles/how-to-select-database-records-in-an-arbitrary-order// here's a similar answer which worked for my pgsql. – Martin Verdejo Sep 18 '18 at 07:01
  • `Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')")` work for me (I am using Postgres) – Sam Kah Chiin Mar 21 '19 at 09:27
  • rails 5.2, it omit the DEPRECATION WARNING message. if the ids is Know-safe values, use wrapping in Arel.sql like Arel.sql("FIELD(id, #{ids.join(', ')})")) – ogelacinyc Nov 07 '19 at 02:57
  • **This is what worked for me ** `ids_in_string_for_query = "'#{ids.join("','")}'"` `User.where(id: ids).order("field(id, #{ids_in_string_for_query})")` – Ankit Wadhwana Apr 13 '20 at 10:33
47

As Mike Woodhouse stated in his answer, this occurs becase, under the hood, Rails is using an SQL query with a WHERE id IN... clause to retrieve all of the records in one query. This is faster than retrieving each id individually, but as you noticed it doesn't preserve the order of the records you are retrieving.

In order to fix this, you can sort the records at the application level according to the original list of IDs you used when looking up the record.

Based on the many excellent answers to Sort an array according to the elements of another array, I recommend the following solution:

Something.find(array_of_ids).sort_by{|thing| array_of_ids.index thing.id}

Or if you need something a bit faster (but arguably somewhat less readable) you could do this:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)
Community
  • 1
  • 1
Ajedi32
  • 45,670
  • 22
  • 127
  • 172
27

This seems to work for postgresql (source) - and returns an ActiveRecord relation

class Something < ActiveRecrd::Base

  scope :for_ids_with_order, ->(ids) {
    order = sanitize_sql_array(
      ["position((',' || id::text || ',') in ?)", ids.join(',') + ',']
    )
    where(:id => ids).order(Arel.sql(order))
  }    
end

# usage:
Something.for_ids_with_order([1, 3, 2])

can be extended for other columns as well, e.g. for the name column, use position(name::text in ?) ...

gingerlime
  • 5,206
  • 4
  • 37
  • 66
  • You are my hero of the week. Thank you! – ntdb Feb 29 '16 at 23:42
  • 4
    Note that this only works in trivial cases, you will eventually run up against a situation where your Id is contained within other IDs in the list (e.g. it will find 1 in 11). One way around this is to add the commas into the position check, and then add a final comma to the join, like this : order = sanitize_sql_array( ["position(','||clients.id::text||',' in ?)", ids.join(',') + ','] ) – IrishDubGuy May 18 '16 at 14:29
  • Good point, @IrishDubGuy! I'll update my answer based on your suggestion. Thanks! – gingerlime May 19 '16 at 19:59
  • for me chaining don't work. Here tables name should be added before id:text like this: `["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ',']` full version that worked for me: `scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) }` thanks @gingerlime @IrishDubGuy – user1136228 Mar 15 '17 at 14:42
  • I guess you need to add the table name in case you do some joins... That's quite common with ActiveRecord scopes when you join. – gingerlime Mar 16 '17 at 16:38
  • This is great, but you can't call things like `.last` on the result: `ActiveRecord::IrreversibleOrderError: Order "position((',' || id::text || ',') in '218,217,216,215,214,')" can not be reversed automatically`. Not a deal-breaker, but something to keep in mind. – t56k Sep 07 '17 at 01:26
  • 2
    Due to deprecation error, I had to change `order(order)` to `order(Arel.sql(order))` to get this to work. – Stan Aug 26 '21 at 21:30
  • @Stan yes, you're absolutely right and that's what we're using now. I forgot to update the answer but will do this now. Thank you. – gingerlime Aug 28 '21 at 06:19
24

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

Something.find(array_of_ids).order_as_specified(id: array_of_ids)

As far as I've been able to test, it works natively in all RDBMSes, and it returns an ActiveRecord relation that can be chained.

Community
  • 1
  • 1
JacobEvelyn
  • 3,901
  • 1
  • 40
  • 51
5

Not possible in SQL that would work in all cases unfortunately, you would either need to write single finds for each record or order in ruby, although there is probably a way to make it work using proprietary techniques:

First example:

sorted = arr.inject([]){|res, val| res << Model.find(val)}

VERY INEFFICIENT

Second example:

unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}
Omar Qureshi
  • 8,963
  • 3
  • 33
  • 35
  • Though not very efficient, I agree this work-around is DB-agnostic and acceptable if you have small amount of rows. – Trung Lê Jun 26 '12 at 06:26
  • Dont' use inject for this, it's a map: `sorted = arr.map { |val| Model.find(val) }` – tokland Mar 12 '13 at 22:04
  • first one is slow. I agree with second one with map like this: ```sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}``` – kuboon Mar 03 '14 at 05:49
5

There is a gem find_with_order which allows you to do it efficiently by using native SQL query.

And it supports both Mysql and PostgreSQL.

For example:

Something.find_with_order(array_of_ids)

If you want relation:

Something.where_with_order(:id, array_of_ids)
khiav reoy
  • 1,373
  • 13
  • 14
4

According to official doc, it should be in the same order.

https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find

Something.find(array_of_ids)

the order of the resulting array should be the same order as array_of_ids. I've tested this in Rails 6.

konyak
  • 10,818
  • 4
  • 59
  • 65
  • This should be the correct answer. ActiveRecord preserves the order by design, so no special methods needed to do so. – jewilmeer Oct 05 '22 at 07:26
2

Under the hood, find with an array of ids will generate a SELECT with a WHERE id IN... clause, which should be more efficient than looping through the ids.

So the request is satisfied in one trip to the database, but SELECTs without ORDER BY clauses are unsorted. ActiveRecord understands this, so we expand our find as follows:

Something.find(array_of_ids, :order => 'id')

If the order of ids in your array is arbitrary and significant (i.e. you want the order of rows returned to match your array irrespective of the sequence of ids contained therein) then I think you'd be best server by post-processing the results in code - you could build an :order clause but it would be fiendishly complicated and not at all intention-revealing.

Mike Woodhouse
  • 51,832
  • 12
  • 88
  • 127
  • Note that the options hash has been deprecated. (second argument, in this example `:order => id`) – ocodo Jul 25 '12 at 02:26
2

@Gunchars answer is great, but it doesn't work out of the box in Rails 2.3 because the Hash class is not ordered. A simple workaround is to extend the Enumerable class' index_by to use the OrderedHash class:

module Enumerable
  def index_by_with_ordered_hash
    inject(ActiveSupport::OrderedHash.new) do |accum, elem|
      accum[yield(elem)] = elem
      accum
    end
  end
  alias_method_chain :index_by, :ordered_hash
end

Now @Gunchars' approach will work

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

Bonus

module ActiveRecord
  class Base
    def self.find_with_relevance(array_of_ids)
      array_of_ids = Array(array_of_ids) unless array_of_ids.is_a?(Array)
      self.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values
    end
  end
end

Then

Something.find_with_relevance(array_of_ids)
Chris Bloom
  • 3,526
  • 1
  • 33
  • 47
2

Assuming Model.pluck(:id) returns [1,2,3,4] and you want the order of [2,4,1,3]

The concept is to to utilize the ORDER BY CASE WHEN SQL clause. For example:

SELECT * FROM colors
  ORDER BY
  CASE
    WHEN code='blue' THEN 1
    WHEN code='yellow' THEN 2
    WHEN code='green' THEN 3
    WHEN code='red' THEN 4
    ELSE 5
  END, name;

In Rails, you can achieve this by having a public method in your model to construct a similar structure:

def self.order_by_ids(ids)
  if ids.present?
    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
else
  all # If no ids, just return all
end

Then do:

ordered_by_ids = [2,4,1,3]

results = Model.where(id: ordered_by_ids).order_by_ids(ordered_by_ids)

results.class # Model::ActiveRecord_Relation < ActiveRecord::Relation

The good thing about this. Results are returned as ActiveRecord Relations (allowing you to use methods like last, count, where, pluck, etc)

Christian Fazzini
  • 19,613
  • 21
  • 110
  • 215
1

Although I don't see it mentioned anywhere in a CHANGELOG, it looks like this functionality was changed with the release of version 5.2.0.

Here commit updating the docs tagged with 5.2.0 However it appears to have also been backported into version 5.0.

XML Slayer
  • 1,530
  • 14
  • 31
0

With reference to the answer here

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") works for Postgresql.

Sam Kah Chiin
  • 4,375
  • 5
  • 25
  • 30
0

I liked the order based options a lot. My 2c is that it makes sense to add it in a scope, so that you can use it chained with other AR methods

scope :find_in_order, ->(ids) { 
  where(id: ids).order([Arel.sql('FIELD(id, ?)'), ids])
}
cmantas
  • 1,516
  • 14
  • 14
-4

There is an order clause in find (:order=>'...') which does this when fetching records. You can get help from here also.

link text

Community
  • 1
  • 1
Ashish Jain
  • 4,667
  • 6
  • 30
  • 35