ActiveRecordで複数項目をdistinctする

1つのカラムに対して重複しないデータを取り出そうとした場合、SQLではdistinctを使います。 それをActiveRecordでやろうとした場合、uniqを使うと思います。

例えば、アンケートシステムで回答したユーザの属性情報が以下のように定義されていた場合を考えてみます。

f:id:taise:20130416072440p:plain

職業(job)の重複無しリストを取得するには以下のように書きます。

[1] pry(main)> User.select(:job).uniq
  User Load (0.2ms)  SELECT DISTINCT job FROM "users"
=> [#<User job: "会社員">, 
#<User job: "公務員">,
 #<User job: "大学生">]

次に、単純に年齢(age)、性別(gender)、職業の3つの属性について取得する場合です。
まずは愚直な書き方をしてみます。
ActiveRecordの場合はfindやwhereなどを使って全ての属性を取得するケースが多いので、このような必要項目のみを取得する方法は、大量データを取得する場合を除いてあまり使われないかもしれません。

[2] pry(main)> User.select("age, gender, job")
  User Load (0.3ms)  SELECT age, gender, job FROM "users"
=> [#<User age: "20", gender: "male", job: "会社員">, 
#<User age: "25", gender: "male", job: "公務員">,
#<User age: "20", gender: "female", job: "大学生">, 
#<User age: "25", gender: "male", job: "公務員">, 
#<User age: "25", gender: "male", job: "公務員">]

今度は、年齢(age)、性別(gender)、職業の3つの属性について重複無しの組み合わせ情報を取得する場合です。いわゆるコンビネーションというやつです。

SQLで書く場合は単純に3つの項目でdistinctすれば良いのですが、上のような書き方では1項目しかとれませんし、select()の引数にシンボルで複数項目を指定するとエラーがでて怒られてしまいます。

[3] pry(main)> User.select(:age, :gender, :job).uniqArgumentError: wrong number of arguments (3 for 1)
from /Users/taise/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/relation/query_methods.rb:70:in `select'

そのため、取得したいカラム名を通常のselect文で書くような形で書いて、「"(ダブルクォーテーション)」でかこったものにuniqをくっつけてあげれば、コンビネーションを取得するクエリが実行されます。

[4] pry(main)> User.select("age, gender, job").uniq  
  User Load (3.4ms)  SELECT DISTINCT age, gender, job FROM "users"
=> [#<User age: "20", gender: "female", job: "大学生">, 
#<User age: "20", gender: "male", job: "会社員">, 
#<User age: "25", gender: "male", job: "公務員">]

ちなみに、select().uniqをつなげる形で書いてあげてもOKです。

[5] pry(main)> User.select(:age).uniq.select(:gender).uniq.select(:job).uniq
  User Load (0.4ms)  SELECT DISTINCT age, gender, job FROM "users"
=> [#<User age: "20", gender: "female", job: "大学生">, 
#<User age: "20", gender: "male", job: "会社員">,
 #<User age: "25", gender: "male", job: "公務員">]

この書き方だと、重複を取り除く問い合わせをしているのに、select().uniqが重複しているというのが皮肉ですが、これでうまく行きます。 where句の条件をつないでも同じような効果が得られるのですが、この辺りのActiveRecordの柔軟性は驚きました。