【问题标题】:Left Join with Group By左加入 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

【问题讨论】:

    标签: sql postgresql group-by left-join greatest-n-per-group


    【解决方案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;
    

    分步说明

    1. 形成给定锻炼和用户的完整笛卡尔积。
      假设给定的锻炼始终存在。
      假设并非所有给定用户都有所有给定锻炼的结果。

    2. LEFT JOINresults。所有条件都进入LEFT JOINON 子句,而不是WHERE 子句,这将排除没有结果的(workout_id, user_id) 组合。见:

    3. 最后选择(user_id, workout_id)DISTINCT ON 的最佳结果。在此过程中,生成所需的排序顺序。见:

    根据表的大小和数据分布,可能会有更快的解决方案。见:

    简单版

    如果你想要的只是每个(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 onrow_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 现在希望自己能走上正确的道路 :)
      猜你喜欢
      • 2012-03-12
      • 1970-01-01
      • 2011-02-07
      • 2019-01-15
      • 2020-12-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-05
      相关资源
      最近更新 更多