左加入 Group By
Posted
技术标签:
【中文标题】左加入 Group By【英文标题】:Left Join with Group By 【发布时间】:2015-05-25 23:05:33 【问题描述】:我使用的是 PostgreSQL 9.4。
我有一张workouts
的表格。用户可以为每个workout
创建多个results
,一个result
有一个score
。
给定一个锻炼 ID 列表和两个用户 ID,我想返回每个用户每次锻炼的最佳分数。如果用户没有该锻炼的结果,我想返回填充/空结果。
SELECT "results".*, "workouts".*
FROM "results" LEFT JOIN "workouts" ON "workouts"."id" = "results"."workout_id"
WHERE (
(user_id, workout_id, score) IN
(SELECT user_id, workout_id, MAX(score)
FROM results WHERE user_id IN (1, 2) AND workout_id IN (1, 2, 3)
GROUP BY user_id, workout_id)
)
在这个查询中,左连接充当内连接;如果用户没有得到锻炼结果,我不会得到任何填充。无论存在多少结果,此查询都应始终返回六行。
示例数据:
results
user_id | workout_id | score
-----------------------------
1 | 1 | 10
1 | 3 | 10
1 | 3 | 15
2 | 1 | 5
Desired result:
results.user_id | results.workout_id | max(results.score) | workouts.name
-------------------------------------------------------------------------
1 | 1 | 10 | Squat
1 | 2 | null | Bench
1 | 3 | 15 | Deadlift
2 | 1 | 5 | Squat
2 | 2 | null | Bench
2 | 3 | null | Deadlift
【问题讨论】:
【参考方案1】:SELECT DISTINCT ON (1, 2)
u.user_id
, w.id AS workout_id
, r.score
, w.name AS workout_name
FROM workouts w
CROSS JOIN (VALUES (1), (2)) u(user_id)
LEFT JOIN results r ON r.workout_id = w.id
AND r.user_id = u.user_id
WHERE w.id IN (1, 2, 3)
ORDER BY 1, 2, r.score DESC NULLS LAST;
分步说明
形成给定锻炼和用户的完整笛卡尔积。 假设给定的锻炼始终存在。 假设并非所有给定用户都有所有给定锻炼的结果。
LEFT JOIN
到 results
。所有条件都进入LEFT JOIN
的ON
子句,而不是WHERE
子句,这将排除没有结果的(workout_id, user_id)
组合。见:
最后选择(user_id, workout_id)
和DISTINCT ON
的最佳结果。在此过程中,生成所需的排序顺序。见:
根据表的大小和数据分布,可能会有更快的解决方案。见:
Optimize GROUP BY query to retrieve latest row per user简单版
如果你想要的只是每个(user_id, workout_id)
组合的最大score
,那么有一个简单的版本:
SELECT user_id, workout_id, max(r.score) AS score
FROM unnest('1,2'::int[]) u(user_id)
CROSS JOIN unnest('1,2,3'::int[]) w(workout_id)
LEFT JOIN results r USING (user_id, workout_id)
GROUP BY 1, 2
ORDER BY 1, 2;
db小提琴here旧sqlfiddle.
【讨论】:
【参考方案2】:使用distinct on
或row_number()
怎么样?
SELECT DISTINCT ON (r.user_id, r.workout_id) r.*, w.*
FROM "results" r LEFT JOIN
"workouts" w
ON "w."id" = r."workout_id"
WHERE r.user_id IN (1, 2) AND r.workout_id IN (1, 2, 3)
ORDER BY r.user_id, r.workout_id, score desc;
row_number()
等效项需要子查询:
SELECT rw.*
FROM (SELECT r.*, w.*,
row_number() over (partition by user_id, workout_id order by score desc) as seqnum
FROM "results" r LEFT JOIN
"workouts" w
ON "w."id" = r."workout_id"
WHERE r.user_id IN (1, 2) AND r.workout_id IN (1, 2, 3)
) rw
WHERE seqnum = 1;
您应该比使用*
更明智地选择列。如果列名重复,子查询可能会返回错误。
编辑:
您需要先生成行,然后再生成每个行的结果。这是一种基于第二个查询的方法:
SELECT u.user_id, w.workout_id, rw.score, rw.name
FROM (SELECT 1 as user_id UNION ALL SELECT 2) u CROSS JOIN
(SELECT 1 as workout_id UNION ALL SELECT 2 UNION ALL SELECT 3) w LEFT JOIN
(SELECT r.*, w.*,
row_number() over (partition by user_id, workout_id order by score desc) as seqnum
FROM "results" r LEFT JOIN
"workouts" w
ON "w."id" = r."workout_id"
WHERE r.user_id IN (1, 2) AND r.workout_id IN (1, 2, 3)
) rw
ON rw.user_id = u.user_id and rw.workout_id = w.workout_id and
rw.seqnum = 1;
【讨论】:
抱歉,如果我误解了您的答案,但这两个查询都只返回两行,而不是所需的六行(需要 2 个 user_ids x 3 个锻炼 IDs = 6 行)。我将用一些示例数据和期望的结果来注释我的原始问题,以使其更清晰。 @BenSmith 。 . .我的误解。我认为最好的是每个用户,而不是每个用户/锻炼。【参考方案3】:where 过滤掉了你的 NULL 值,这就是为什么结果不是你所期望的。
加入 WHERE 子句结果,而不是过滤 where 子句结果。
SELECT "results".*, "workouts".*,"max_score".*
FROM "results"
LEFT JOIN "workouts" ON "workouts"."id" = "results"."workout_id"
LEFT JOIN (SELECT user_id, workout_id, MAX(score)
FROM results WHERE user_id IN (1, 2) AND workout_id IN (1, 2, 3)
GROUP BY user_id, workout_id) max_score ON workouts.workout_id=max_score.workout_id;
您需要更改 SELECT 以获得正确的列。
【讨论】:
这将为我返回未遵守 user_id IN (1, 2) 和锻炼 ID IN (1, 2, 3) 约束的行。 所以连接应该在 user_id 和锻炼 ID 上而不是结果表? 我认为max_score
的左连接需要在(workout_id,user_id,score)
上
@FuzzyTree :你可能是对的。示例数据是在答案之后添加的。 Ben 现在希望自己能走上正确的道路 :)以上是关于左加入 Group By的主要内容,如果未能解决你的问题,请参考以下文章