I have the model Subscription
which has_many
Version
s.
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.