Home > Back-end >  How can I find an associated table oldest record filtering by one of it's attributes?
How can I find an associated table oldest record filtering by one of it's attributes?

Time:11-24

I have the model Subscription which has_many Versions.

A Version has a status, plan_id and authorized_at date.

Any changes made to a Subscription comes from a Version modifications updating it's parent Subscription.

The goal is to find each Subscription's Version with the oldest authorized_at date WHERE the versions.plan_id is the same as the subscriptions.plan_id (in other words I need the authorization date of the Version where the plan_id changed to the current Subscription's plan_id).

This is the query I've come up with. I'm getting an error in the aggregate function syntax:

syntax error at or near "MIN" LINE 3: MIN (authorized_at) from versions ^

query:

select subscriptions.id,
MIN (authorized_at) from versions
where versions.plan_id = subscriptions.plan_id
) as current_version
from subscriptions
join versions on subscriptions.id = versions.subscription_id
where versions.status = 'processed'

I also am not sure if I should be grouping the versions by plan_id and then picking from each group. I'm kind of lost.

CodePudding user response:

You can use DISTINCT ON to filter out rows, and keep a single one per subscription -- the first one per group according to the ORDER BY clause.

For example:

select distinct on (s.id) s.id, v.authorized_at
from subscription s
join versions v on v.subscription_id = s.id and v.plan_id = s.plan_id
where v.status = 'processed'
order by s.id, v.authorized_at

CodePudding user response:

Below code will give you the versions where Version's plan_id is equal to Subscription's plan_id.

@versions = Version.joins("LEFT JOIN subscriptions ON subscriptions.plan_id = versions.plan_id")

To filter records by Version's status

@versions = Version.joins("LEFT JOIN subscriptions ON subscriptions.plan_id = versions.plan_id").where(status: "processed")

To filter records by Version's status and order by authorized_at in ascending order.

@versions = Version.joins("LEFT JOIN subscriptions ON subscriptions.plan_id = versions.plan_id").where(status: "processed").order(:authorized_at)

To filter records by Version's status and order by authorized_at in decending order.

@versions = Version.joins("LEFT JOIN subscriptions ON subscriptions.plan_id = versions.plan_id").where(status: "processed").order(authorized_at: :desc)

Hope this works for you!

CodePudding user response:

You can use a lateral subquery which can best be described as a foreach loop in SQL. They are an extremly performant way to select columns from a single correlated record or even aggregates from a group of related records.

For each row in subscriptions the DB will select a single row from versions ordered by authorized_at:

SELECT "subscriptions".*,
       "latest_version"."authorized_at" AS current_version,
       "latest_version"."id" AS current_version_id -- could be very useful
FROM   "subscriptions" 
LATERAL
  (
     SELECT   "versions"."authorized_at", "versions"."id"
     FROM     "versions"
     WHERE    "versions"."subscription_id" = "subscriptions"."id" -- lateral reference
     AND      "versions"."plan_id" = "subscriptions"."plan_id"
     AND      "versions"."status" = 'processed'
     ORDER BY "versions"."authorized_at" ASC
     LIMIT 1
  ) latest_version ON TRUE

Creating lateral joins in ActiveRecord can be done either with SQL strings or Arel:

class Subscription < ApplicationRecord
  # Performs a lateral join and selects the 
  # authorized_at of the latest version 
  def self.with_current_version
    lateral = Version.arel_table.then do |v|
      v.project(
        v[:authorized_at],
        v[:id] # optional
      ).where(
        v[:subscription_id].eq(arel_table[:id])
          .and(v[:plan_id].eq(arel_table[:plan_id]) )
          .and(v[:status].eq('processed'))
      )
      .order(v[:authorized_at].asc)
      .take(1) # limit 1
      .lateral('latest_version ON TRUE')
    end
    lv = Arel::Table.new(:latest_version) # just a table alias
    select(
      *where(nil).arel.projections, # selects everything previously selected
      lv[:authorized_at].as("current_version"),
      lv[:id].as("current_version_id") # optional
    ).joins(lateral.to_sql)
  end
end

If you just want to select the id and current_version column you should consider using pluck instead of selecting database models that aren't properly hydrated.

  • Related