Fetching Minimum/Maximum for each group in ActiveRecord
Asked Answered
R

6

12

This is an age-old question where given a table with attributes 'type', 'variety' and 'price', that you fetch the record with the minimum price for each type there is.

In SQL, we can do this by:

select f.type, f.variety, f.price   
from (  select type, min(price) as minprice from table group by type ) as x  
inner join table as f on f.type = x.type and f.price = x.minprice;`

We could perhaps imitate this by:

minprices = Table.minimum(:price, :group => type)  
result = []
minprices.each_pair do |t, p|  
   result << Table.find(:first, :conditions => ["type = ? and price = ?", t, p])
end

Is there a better implementation than this?

Ratline answered 9/10, 2008 at 1:12 Comment(1)
how to get maximum and minimum price for each type ??Ivette
O
12
Table.minimum(:price, :group => :type)

See http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-minimum for more.

On answered 9/10, 2008 at 1:55 Comment(2)
how to get maximum and minimum price for each type ??Ivette
For some reason the :group => :type part is being ignored, for me.Milkfish
F
3

This worked for me.

Table.group(:type).minimum(:price)

And it returns an object like this.

{
 "type1"=>500.0,
 "type2"=>200.0
}
Forsaken answered 25/2, 2021 at 1:5 Comment(0)
D
1

You can use #find_by_sql, but this implies returning a model object, which might not be what you want.

If you want to go bare to the metal, you can also use #select_values:

data = ActiveRecord::Base.connection.select_values("
        SELECT f.type, f.variety, f.price
        FROM (SELECT type, MIN(price) AS minprice FROM table GROUP BY type ) AS x
        INNER JOIN table AS f ON f.type = x.type AND f.price = x.minprice")
puts data.inspect
[["type", "variety", 0.00]]

ActiveRecord is just a tool. You use it when it's convenient. When SQL does a better job, you use that.

Diskson answered 9/10, 2008 at 7:6 Comment(0)
S
1

I've been fighting with this for a while and for the moment it seems that you are pretty much stuck with generating SQL.

However, I have a couple refinements to offer.

Instead of find_by_sql, as @François suggested, I've used ActiveRecord's to_sql and joins to "guide" my SQL a little bit:

subquery_sql = Table.select(["MIN(price) as price", :type]).group(:type).to_sql
joins_sql    = "INNER JOIN (#{subquery_sql}) as S
                ON table.type = S.type
                AND table.price = S.price"

Table.joins(joins_sql).where(<other conditions>).order(<your order>)

As you can see, I'm still using raw SQL, but at least it's only on the part where AR gives no support (AFAIK ActiveRecord simply can't manage INNER JOIN ... ON ...) and not on the whole thing.

Using joins instead of find_by_sql makes the query chainable - you can add extra conditions, or sort the table, or put everything in a scope.

Shutout answered 14/2, 2013 at 18:45 Comment(0)
I
0

While this question is pretty stale, I was asking the same question today. Here's a gist of a solution to compose the SQL needed to accomplish the goal with minimal (2) queries.

Please lmk if there are better ways these days!

Using Security and Price models, where Securities have many (historical) Prices, and you are after the Securities' most recent price:

module MostRecentBy
  def self.included(klass)
    klass.scope :most_recent_by, ->(group_by_col, max_by_col) {
      from(
        <<~SQL
          (
            SELECT #{table_name}.*
            FROM #{table_name} JOIN (
               SELECT #{group_by_col}, MAX(#{max_by_col}) AS #{max_by_col}
               FROM #{table_name}
               GROUP BY #{group_by_col}
            ) latest
            ON #{table_name}.date = latest.#{max_by_col}
            AND #{table_name}.#{group_by_col} = latest.#{group_by_col}
          ) #{table_name}
        SQL
      )
    }
  end
end

class Price < ActiveRecord::Base
  include MostRecentBy

  belongs_to :security

  scope :most_recent_by_security, -> { most_recent_by(:security_id, :date) }
end

class Security < ActiveRecord::Base
  has_many :prices
  has_one :latest_price, 
    -> { Price.most_recent_by_security },
    class_name: 'Price'
end

now you can call the following in your controller code:

def index
  @resources = Security.all.includes(:latest_price)

  render json: @resources.as_json(include: :latest_price)
end

which results in two queries:

  Security Load (4.4ms)  SELECT "securities".* FROM "securities"
  Price Load (140.3ms)  SELECT "prices".* FROM (
    SELECT prices.*
    FROM prices JOIN (
       SELECT security_id, MAX(date) AS date
       FROM prices
       GROUP BY security_id
    ) latest
    ON prices.date = latest.date
    AND prices.security_id = latest.security_id
  ) prices
  WHERE "prices"."price_type" = $1 AND "prices"."security_id" IN (...)

for reference: https://gist.github.com/pmn4/eb58b036cc78fb41a36c56bcd6189d68

Indiaindiaman answered 28/4, 2020 at 18:31 Comment(0)
Y
-1

To update Avdi's answer above:

Table.minimum(:price, :group => :type)

Here is the updated URL:

http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-minimum

Yea answered 10/6, 2014 at 8:3 Comment(2)
Perhaps a link to a more up-to-date version of Rails would be helpful. Not sure why you chose to link to 2 major versions ago.Cassilda
Thanks Isaac. Updated.Yea

© 2022 - 2024 — McMap. All rights reserved.