问题
How can I turn the following SQL query into an ActiveRecord relation so that I can expand on it with scopes?
WITH joined_table AS (
SELECT workout_sets.weight AS weight,
workouts.user_id AS user_id,
workouts.id AS workout_id,
workout_sets.id AS workout_set_id,
workout_exercises.exercise_id AS exercise_id
FROM workouts
INNER JOIN workout_exercises ON workout_exercises.workout_id = workouts.id
INNER JOIN workout_sets ON workout_sets.workout_exercise_id = workout_exercises.id
ORDER BY workout_sets.weight DESC
),
sub_query AS (
SELECT p.user_id, MAX(weight) as weight
FROM joined_table p
GROUP BY p.user_id
),
result_set AS (
SELECT MAX(x.workout_id) AS workout_id, x.user_id, x.weight, x.workout_set_id, x.exercise_id
FROM joined_table x
JOIN sub_query y
ON y.user_id = x.user_id AND y.weight = x.weight
GROUP BY x.user_id, x.weight, x.workout_set_id, x.exercise_id
ORDER BY x.weight DESC)
SELECT workouts.*, result_set.weight, result_set.workout_set_id, result_set.exercise_id
FROM workouts, result_set
WHERE workouts.id = result_set.workout_id
Is this something I would have to attempt with straight ARel?
I've tried breaking it up into scopes/subqueries, but the selects on the subqueries end up in the enclosing query, thus throwing PostgreSql errors because the column isn't specified in the GROUP BYs or ORDER BYs in the enclosing statement.
Update:
You are correct in your assumption that it's PostgreSql. I attempted your query, but it throws a PG::Error: ERROR: column "rownum" does not exist
, for both the straight query and the ActiveRecord equivalence.
However, when I wrap the query in a separate query, it works. I'm assuming that the ROW_NUMBER() doesn't get created until after the select is projected onto the data set. So the following query works:
SELECT workouts.*, t.weight, t.workout_set_id, t.exercise_id, t.row_num
FROM workouts,
(SELECT workouts.id as workout_id, workout_sets.weight as weight,
workout_sets.id AS workout_set_id,
workout_exercises.id AS exercise_id,
ROW_NUMBER() OVER (
PARTITION BY workouts.user_id
ORDER BY workout_sets.weight DESC, workouts.id DESC ) row_num
FROM workouts
JOIN workout_exercises ON workout_exercises.workout_id = workouts.id
JOIN workout_sets ON workout_sets.workout_exercise_id = workout_exercises.id) as t
WHERE workouts.id = t.workout_id AND t.row_num = 1
Which I've managed to massage into the following:
selected_fields = <<-SELECT
workouts.id AS workout_id,
workout_sets.weight AS weight,
workout_sets.id AS workout_set_id,
workout_exercises.id AS exercise_id,
ROW_NUMBER() OVER (
PARTITION BY workouts.user_id
ORDER BY workout_sets.weight DESC, workouts.id DESC) as row_num
SELECT
Workout.joins(", (#{Workout.joins(:workout_exercises => :workout_sets).select(selected_fields).to_sql}) as t").select("workouts.*, t.*").where("workouts.id = t.workout_id AND t.row_num = 1").order("t.weight DESC")
But as you can tell, that's extremely hacky and is a massive code smell. Any idea as to how to refactor that?
回答1:
You are apparently trying to get the latest workout (highest id) details that match the highest weight for each user. It also appears that you are using PostgreSQL (MySQL doesn't have CTE's), correct me if I'm wrong on this.
If so, you can make use of windowing functions and simplify your query to:
SELECT * FROM (
SELECT workouts.*, workout_sets.weight,
workout_sets.id AS workout_set_id,
workout_exercises.id AS exercise_id,
ROW_NUMBER() OVER (
PARTITION BY workouts.user_id
ORDER BY workout_sets.weight DESC, workouts.id DESC ) as rowNum
FROM workouts
JOIN workout_exercises ON workout_exercises.workout_id = workouts.id
JOIN workout_sets ON workout_sets.workout_exercise_id = workout_exercises.id
) t
WHERE rowNum = 1
Which in ActiveRecord can be written as:
selected_fields = <<-SELECT
workouts.*,
workout_sets.weight,
workout_sets.id AS workout_set_id,
workout_exercises.id AS exercise_id,
ROW_NUMBER() OVER (
PARTITION BY workouts.user_id
ORDER BY workout_sets.weight DESC, workouts.id DESC) as rowNum
SELECT
subquery = Workout.joins(:workout_exercises => :workout_sets).
select(selected_fields).to_sql
Workout.select("*").from(Arel.sql("(#{subquery}) as t"))
.where("rowNum = 1")
来源:https://stackoverflow.com/questions/16324305/turn-sql-query-into-activerecord-relation