Fetching Minimum/Maximum for each group in ActiveRecord

前端 未结 5 1281
说谎
说谎 2021-02-05 20:53

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.

相关标签:
5条回答
  • 2021-02-05 21:27

    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

    0 讨论(0)
  • 2021-02-05 21:30

    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.

    0 讨论(0)
  • 2021-02-05 21:38

    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

    0 讨论(0)
  • 2021-02-05 21:42

    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.

    0 讨论(0)
  • 2021-02-05 21:44
    Table.minimum(:price, :group => :type)
    

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

    0 讨论(0)
提交回复
热议问题