Fetching Minimum/Maximum for each group in ActiveRecord

前端 未结 5 1280
说谎
说谎 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

提交回复
热议问题