重写 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 BY
和 SELECT
子句不兼容。
您似乎想要满足过滤条件的每个用户的最高分行。更好的方法是使用 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 子句和聚合函数的不可预测的行为。
我最近将 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 BY
和 SELECT
子句不兼容。
您似乎想要满足过滤条件的每个用户的最高分行。更好的方法是使用 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 子句和聚合函数的不可预测的行为。