How to select an array of values in Rails

I recently wanted to get a list of IDs from rails, but wanted them in an array.

First try is to use :select => ‘id’ in:

Model.find(:all, :select => 'id')

but that creates a model for each id

In order to get an array of values, you can use select_values:

Model.connection.select_values('select id from models')

This is great, but what if you have a more complex query and you still want to use the query building in Rails finder methods.
You can use construct_finder_sql. This is a private method so it needs to be called via send and you should be cautious about it’s availability in future versions of Rails.
The final query then is:

sql = Model.send(:construct_finder_sql, :select => 'id', :conditions => [...])
Model.connection.select_values(sql)

This will be quicker and use less memory because it doesn’t create a model object for each id.
I would only use this when pulling fairly large arrays of values, stick to the normal way of doing things for smaller collections.

If you want to get two or three values per ‘row’, you can use select_rows which will return an array of arrays.

Comments (13)

PeterJune 15th, 2009 at 16:48

I found it was nice to wrap these in an AR::B extension, so that Model.find_values basically does Model.connect.select_values(construct_finder_sql…). Better abstracted and also this way it plays nice with will_paginate; having Model.find_values immediately allows you to do Model.paginate_values as well.

Jeffrey ChuppJune 15th, 2009 at 17:12

echoing what Peter said, this seems to work nicely (provided you only pass one attribute for :select): http://gist.github.com/130161

Jeffrey ChuppJune 15th, 2009 at 17:16

One more thing: Model.find(:all, :select => ‘id’) returns objects, but that object’s attributes are the right data type. In using select_values, you’ll always get back strings, which may or may not matter. Something to keep in mind…

MaiaJune 15th, 2009 at 18:10

Here’s my take: http://gist.github.com/130188
It adds the ActiveRecord::Base methods select_value, select_values, select_all and select_rows including :limit, :select (also multiple), :order etc, and it will also convert the returned strings into whatever type they should be.
Feel free to optimize or convert it into a tiny gem/plugin, I just placed it in config/initializers.

[...] How to select an array of values in Rails @ Ramblings on Rails [...]

TrazJune 15th, 2009 at 23:34

A simple use for small collections (your query for big one is better i a assume) :

Model.all.map(&:id) #=> return an array

and it work with a subset defined by a named scope.

MaiaJune 16th, 2009 at 00:24

Traz, the entire idea of this post is to prevent ruby from constructing many objects which aren’t required (which your suggestion still does). And by the way, the solution I provided above will work with named scopes too, e.g.:

Model.public.since(some_date).select_values(:select => :something, :limit => 100)

will work and return an array without constructing all the objects first.

PeterJune 16th, 2009 at 16:14

Maia, doing the type_cast is a nice feature, but if you benchmark it you’ll probably find it’s SLOWER than just letting rails build its objects. Because Rails doesn’t do the typecasting until you actually call for the attribute. I like to leave the typecasting as an opt-in on the method.

PeterJune 16th, 2009 at 16:17

One last point about typecasting; if you look in the Rails source, you’ll notice it also checks against serialized columns and makes a call to deserialize them when the attribute is called for. So if you want very thorough typecasting on the returns this is probably worth having as well.

_KevinJune 16th, 2009 at 23:41

If the objects you want ids for are associated with another object you can do

Model.other_model_ids … not quite what you wanted.

I’ve also used:

Model.find(:all, :conditions=>’…’, :select=>:id).map &:id

Which still creates objects but doesn’t fully hydrate them and then throws them away immediately.

MaiaJune 18th, 2009 at 00:33

Peter, my suggestion was less about speed but about memory usage and garbage collection. I have a daemon process running which used to grow to hundreds of MB within a week (when going the ‘normal’ way of constructing object like _Kevin suggested above), and with the above solution I can keep it from growing (and it’s faster too - but I know it can always be even faster).

Besides that, if I do a select_values I usually want to process the returned array immediately, therefor I’m unsure what you’re suggestion about opting in on the typecasting is really about - what did I miss?

But I’d really appreciate if someone is willing to optimize the suggested code, as I’m everything else but an expert in ruby and/or ActiveRecord. :)

PeterJune 21st, 2009 at 19:42

Hi Maia, that makes sense. What I meant was just that most places where I use my find_values or find_rows, I’m able to hard code in the typecasting after the call, or I know that its okay to just use the value as a string so it doesn’t need to be typecast. And I’m interested in getting the speed boost if I can. So my methods skip the typecasting unless it is explicitly asked for.

Andrew TimberlakeJune 22nd, 2009 at 15:19

@jeffery Nice way to wrap it for re-use. You could add a check for a ‘,’ in :select and change it to select_rows for more dynamic support.

@maia I personally wouldn’t use the type_cast because you’re typically getting a set of values that you know the type of and want to use quickly.
You could easily do a select_values(…).map{|id| id.to_i}