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.
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
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.
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
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.
Table.minimum(:price, :group => :type)
See http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-minimum for more.