重写 SQL 查询以修复由 MySQL 5.7 严格模式引起的功能依赖问题

Rewrite SQL query to Fix Functional Dependency Issue Caused By MySQL 5.7 Strict Mode

我最近将 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

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

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

当您聚合 (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 中的 运行ning 示例。

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

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

那是因为您从一开始就真的不想聚合。因此,您使用了允许语法的 MySQL 扩展——即使根据 SQL 的定义它是错误的:GROUP BYSELECT 子句不兼容。

您似乎想要满足过滤条件的每个用户的最高分行。更好的方法是使用 window 函数:

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

使用用户定义的变量和旧版本 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 子句和聚合函数的不可预测的行为。