2

我最近将我的 MySQL 服务器升级到 5.7 版,但以下示例查询不起作用:

SELECT * 
FROM (SELECT * 
        FROM exam_results 
        WHERE exam_body_id = 6674 
        AND exam_date >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK) 
        AND subject_ids LIKE '%4674%' 
        ORDER BY score DESC 
    ) AS top_scores 
GROUP BY user_id 
ORDER BY percent_score DESC, time_advantage DESC 
LIMIT 10

该查询应该从指定的表中选择与在某个时间间隔内完成特定考试的最高得分者匹配的考试结果。我在第一次编写查询时必须包含 GROUP BY 子句的原因是为了消除重复用户,即在同一时间段内有多个最高分的用户参加考试。在不消除重复用户 ID 的情况下,前 10 名高分者的查询可能会返回同一个人的考试结果。

我的问题是:如何重写此查询以消除与 MySQL 5.7 严格模式相关的错误,该模式在 GROUP BY 子句上强制执行,同时仍保留我想要的功能?

4

3 回答 3

2

当您GROUP BY按列的子集 ( ) 聚合 ( ) 结果集时,user_id需要聚合所有其他列。

注意:根据 SQL 标准,如果您按主键分组,则没有必要这样做,因为所有其他列都依赖于 PK。但是,您的问题并非如此。

现在,您可以使用任何聚合函数,如MAX()MIN()SUM()等。我选择使用MAX(),但您可以为其中任何一个更改它。

查询可以运行为:

SELECT 
  user_id,
  max(exam_body_id),
  max(exam_date),
  max(subject_ids),
  max(percent_score),
  max(time_advantage)
FROM exam_results 
WHERE exam_body_id = 6674 
  AND exam_date >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK) 
  AND subject_ids LIKE '%4674%' 
GROUP BY user_id 
ORDER BY max(percent_score) DESC, max(time_advantage) DESC 
LIMIT 10

请参阅DB Fiddle上的运行示例。

现在,您问为什么需要聚合其他列?由于您正在对行进行分组,因此引擎需要为每组生成一行。因此,当有许多值可供选择时,您需要告诉引擎选择哪个值:最大的值、最小的值、它们的平均值等。

在 MySQL 5.7.4 或更早版本中,引擎不要求您聚合其他列。引擎默默随机为你决定。您今天可能已经得到了您想要的结果,但明天引擎可能会在您不知情的情况下选择 theMIN()而不是 the MAX(),因此每次运行查询时都会导致不可预测的结果。

于 2021-03-07T18:04:55.470 回答
2

那是因为你从来没有真正想要聚合开始。因此,您使用了允许您的语法的 MySQL 扩展——即使 SQL 的定义是错误的:GROUP BYandSELECT子句不兼容。

您似乎希望每个满足过滤条件的用户得分最高的行。更好的方法是使用窗口函数:

SELECT er.* 
FROM (SELECT er.*,
             ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY score DESC) as seqnum
      FROM exam_results er 
      WHERE exam_body_id = 6674 AND
            exam_date >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK) AND
            subject_ids LIKE '%4674%' 
    ) er
WHERE seqnum = 1
ORDER BY percent_score DESC, time_advantage DESC 
LIMIT 10;

你可以在旧版本的 MySQL 中做类似的事情。可能最接近的方法使用变量:

SELECT er.*,
       (@rn := if(@u = user_id, @rn + 1,
                  if(@u := user_id, 1, 1)
                 )
       ) as rn
FROM (SELECT er.*
      FROM exam_results 
      WHERE exam_body_id = 6674 AND
            exam_date >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK) AND
            subject_ids LIKE '%4674%' 
      ORDER BY user_id, score DESC
     ) er CROSS JOIN
     (SELECT @u := -1, @rn := 0) params
HAVING rn = 1
ORDER BY percent_score DESC, time_advantage DESC 
LIMIT 10
于 2021-03-07T19:26:32.140 回答
0

使用用户定义变量和旧版本 MySQL 的 CASE 条件语句替代 Gordon 的答案如下:

SELECT *
    FROM (
        SELECT *,
            @row_number := CASE WHEN @user_id <> er.user_id 
                                THEN 1 
                                ELSE @row_number + 1 END 
                           AS row_number,
            @user_id := er.user_id
        FROM exam_results er
        CROSS JOIN (SELECT @row_number := 0, @user_id := null) params
            WHERE exam_body_id = 6674 AND
            exam_date >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK) AND
            subject_ids LIKE '%4674%' 
        ORDER BY er.user_id, score DESC
    ) inner_er
HAVING inner_er.row_number = 1
ORDER BY score DESC, percent_score DESC, time_advantage DESC 
LIMIT 10

这实现了我想要的过滤行为,而不必依赖 GROUP BY 子句和聚合函数的不可预测行为。

于 2021-03-08T10:11:18.383 回答