开发者

SQL Statement for first row in a "has_many"-association

I'm trying to access only the first lines of associated tables. What is th开发者_如何学Pythone best and fastest way to do this? (Ideally an SQL statement)

Associations :

class User
  has_many :addresses


class Address
  belongs_to :user

The fastest way I could figure out was:

User.includes(:addresses).find(:all).map {|m| m.addresses.first.area_code}

But this operation is too slow for my requirements (imagine I have 200 Users with an average of 20 Adresses (~4000 Adresses overall))

This is just an example of what I am trying to do.

I thought that this could be expressed by an SQL JOIN statement, but I'm not seeing how to receive a table of every users first Address.

Example of expected output:

12345 #=>User.find(1).addresses.first.area_code

56789 #=>User.find(2).addresses.first.area_code 

23456 #=>User.find(3).addresses.first.area_code 

.....


area_codes = 
  Address.find_by_sql(
    "SELECT DISTINCT user_id, area_code FROM addresses"
  ).index_by(&:user_id)
User.find(:all).map{ |u| [u, area_codes[u.id]] }

if you want all of them in one piece.


There's a couple of approaches you can take for this. Even though 4000 records isn't all that much in the scheme of things, avoiding having to visit in multiple queries (like you hinted in your question) is a "good thing".

A key to the solution is how you choose which address you want to look at. "First" is relative and non-deterministic without a qualifier (order by, or some other attribute).

Simple Approach - has_one association

One simple approach is to define a has_one relationship, e.g.

class User
  has_many :addresses
  has_one :primary_address, :class_name => 'Address', :conditions => ["primary = ?", true]
end

That's assuming you have an attribute in Address you can use a decider. If you don't specify a condition, you'll just get whichever address entry comes "first" since has_one applies a SQL LIMIT 1 to the underlying query.

This makes it quite easy to collect users and a single address details:

# iterating over users
User.scoped.each { |user| puts user.primary_address.area_code }
# or collect area_codes
area_codes = User.scoped.collect{ |user| user.primary_address.area_code }

However, this is still visiting each row, so there will be an extra query (and ruby array handling) for every user's address.

Much Better - Arel

Once you've selceted the deciding criteria between the multiple addresses a user may have, it's possible to roll this up into a single query.

If I assume we'll just select the address by min(id) - and assuming you're on Rails 3 - here's an Arel approach for querying the distinct area_codes:

address = Address.arel_table
primary_addresses = Address.where(
  address[:id].in(
    address.group(address[:user_id]).project(address[:id].minimum)
  )
)

primary_addresses is then a scope that gives you the full collection of 'primary' addresses for users. You can then process/list/collect as you need e.g.

area_codes = primary_addresses.collect(&:area_code)

The query will actually generate SQL like this:

SELECT "addresses".* 
FROM "addresses" 
WHERE "addresses"."id" IN (
  SELECT MAX("addresses"."id") AS max_id FROM "addresses" GROUP BY "addresses"."user_id"
)

As you can see, the sub-select is returning the list of address id's - one for each user. If you want to use a different criteria for selecting the address to use, you can change the sub-query (e.g. to make it select on primary=true with limit 1)

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜