允许在 HAVING 子句中使用别名的性能影响
Performance implications of allowing alias to be used in HAVING clause
我今天早些时候在 this question 上出丑了。问题是使用 SQL 服务器,正确答案涉及添加 HAVING
子句。我最初犯的错误是认为 SELECT
语句中的别名可以用在 HAVING
子句中,这在 SQL Server 中是不允许的。我犯了这个错误,因为我假设 SQL 服务器具有与 MySQL 相同的规则,这确实允许在 HAVING
子句中使用别名。
这让我很好奇,我在 Stack Overflow 和其他地方四处寻找,找到一堆 material 解释为什么这些 规则 在两个相应的 RDBMS 上强制执行.但是我在任何地方都找不到 performance 含义的解释 allowing/disallowing HAVING
子句中的别名。
举一个具体的例子,我将复制上述问题中出现的查询:
SELECT students.camID, campus.camName, COUNT(students.stuID) as studentCount
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING COUNT(students.stuID) > 3
ORDER BY studentCount
在 HAVING
子句中使用别名而不是重新指定 COUNT
会对性能产生什么影响?这个问题可以在 MySQL 中直接回答,希望有人能深入了解 SQL 中如果支持 HAVING
子句中的别名会发生什么。
在这种情况下,可以在 MySQL 和 SQL 服务器上标记 SQL 问题的情况很少见,所以享受这阳光下的时刻吧。
我不认为真的有任何性能影响,除非 having
子句中的表达式包含复杂的处理(例如,count(distinct)
或复杂的函数,例如字符串处理长字符串)。
我几乎可以肯定,如果在查询中提到两次,MySQL 将执行两次聚合函数。我不确定 SQL 服务器是否会优化第二个引用,但我猜不会(SQL 服务器有一个很好的优化器,但它不是很好的通用表达式消除)。
接下来的问题是表达式的复杂性。 count()
和 sum()
等简单表达式实际上不会产生太多额外开销——一旦聚合已经完成。复杂的表达式可能开始变得昂贵。
如果您在 SQL 服务器中有一个复杂的表达式,您应该能够通过使用子查询来保证它只被计算一次。
我原以为 SQL 会按 FROM
、WHERE
、GROUP BY
、HAVING
、SELECT
的顺序进行ORDER BY
我不是 MYSQL 专家,但在 MYSQL Documentation 中找到了为什么它是合法的原因。
MySQL 扩展了 GROUP BY 的标准 SQL 使用,因此 select 列表可以引用 GROUP BY 子句中未命名的非聚合列。这意味着前面的查询在 MySQL 中是合法的。 您可以使用此功能通过避免不必要的列排序和分组来获得更好的性能。但是,当 GROUP BY 中未命名的每个非聚合列中的所有值都相同时,这主要有用每组。服务器可以自由地从每个组中选择任何值,因此除非它们相同,否则所选的值是不确定的。此外,添加 ORDER BY 子句不会影响来自每个组的 selection 值。结果集排序发生在选择值之后,ORDER BY 不影响服务器在每个组中选择的值。
类似的 MySQL 扩展适用于 HAVING 子句 。在标准 SQL 中,查询不能引用 HAVING 子句中未在 GROUP BY 子句中命名的非聚合列。为了简化计算,MySQL 扩展允许引用此类列。此扩展假定未分组的列具有相同的分组值。否则,结果不确定。
关于性能影响,我假设别名 having 会比非别名 having 慢,因为必须在所有执行后应用过滤器。我会等待专家发表评论。
狭隘地关注特定查询,并在下面加载示例数据。这确实解决了其他一些查询,例如其他人提到的 count(distinct ...)
。
alias in the HAVING
似乎略优于或相当优于其替代方案(取决于查询)。
这使用了一个预先存在的 table,其中包含大约 500 万行,通过我的这个 快速创建,需要 3 到 5 分钟。
结果结构:
CREATE TABLE `ratings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`thing` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5046214 DEFAULT CHARSET=utf8;
而是改用 INNODB。由于 运行ge 保留插入,创建预期的 INNODB 间隙异常。只是说,但没有区别。 470 万行。
修改 table 以接近 Tim 的假定架构。
rename table ratings to students; -- not exactly instanteous (a COPY)
alter table students add column camId int; -- get it near Tim's schema
-- don't add the `camId` index yet
以下内容需要一段时间。 运行 一次又一次地分块进行,否则您的连接可能会超时。超时是由于更新语句中没有 LIMIT 子句的 500 万行。请注意,我们 有一个 LIMIT 子句。
所以我们要进行 50 万行迭代。将列设置为
运行dom 数字在 1 到 20 之间
update students set camId=floor(rand()*20+1) where camId is null limit 500000; -- well that took a while (no surprise)
保持运行以上直到没有camId
为空。
我运行喜欢10次(整个过程需要7到10分钟)
select camId,count(*) from students
group by camId order by 1 ;
1 235641
2 236060
3 236249
4 235736
5 236333
6 235540
7 235870
8 236815
9 235950
10 235594
11 236504
12 236483
13 235656
14 236264
15 236050
16 236176
17 236097
18 235239
19 235556
20 234779
select count(*) from students;
-- 4.7 Million rows
创建一个有用的索引(当然是在插入之后)。
create index `ix_stu_cam` on students(camId); -- takes 45 seconds
ANALYZE TABLE students; -- update the stats: http://dev.mysql.com/doc/refman/5.7/en/analyze-table.html
-- the above is fine, takes 1 second
创建校园table。
create table campus
( camID int auto_increment primary key,
camName varchar(100) not null
);
insert campus(camName) values
('one'),('2'),('3'),('4'),('5'),
('6'),('7'),('8'),('9'),('ten'),
('etc'),('etc'),('etc'),('etc'),('etc'),
('etc'),('etc'),('etc'),('etc'),('twenty');
-- ok 20 of them
运行 两个查询:
SELECT students.camID, campus.camName, COUNT(students.id) as studentCount
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING COUNT(students.id) > 3
ORDER BY studentCount;
-- run it many many times, back to back, 5.50 seconds, 20 rows of output
和
SELECT students.camID, campus.camName, COUNT(students.id) as studentCount
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING studentCount > 3
ORDER BY studentCount;
-- run it many many times, back to back, 5.50 seconds, 20 rows of output
所以时间是一样的。 运行各十几次。
EXPLAIN
两者的输出相同
+----+-------------+----------+------+---------------+------------+---------+----------------------+--------+---------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------+------+---------------+------------+---------+----------------------+--------+---------------------------------+
| 1 | SIMPLE | campus | ALL | PRIMARY | NULL | NULL | NULL | 20 | Using temporary; Using filesort |
| 1 | SIMPLE | students | ref | ix_stu_cam | ix_stu_cam | 5 | bigtest.campus.camID | 123766 | Using index |
+----+-------------+----------+------+---------------+------------+---------+----------------------+--------+---------------------------------+
使用 AVG() 函数,通过以下两个查询中 having
中的别名(具有相同的 EXPLAIN
输出),我的性能提高了大约 12%。
SELECT students.camID, campus.camName, avg(students.id) as studentAvg
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING avg(students.id) > 2200000
ORDER BY students.camID;
-- avg time 7.5
explain
SELECT students.camID, campus.camName, avg(students.id) as studentAvg
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING studentAvg > 2200000
ORDER BY students.camID;
-- avg time 6.5
最后,DISTINCT
:
SELECT students.camID, count(distinct students.id) as studentDistinct
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID
HAVING count(distinct students.id) > 1000000
ORDER BY students.camID; -- 10.6 10.84 12.1 11.49 10.1 9.97 10.27 11.53 9.84 9.98
-- 9.9
SELECT students.camID, count(distinct students.id) as studentDistinct
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID
HAVING studentDistinct > 1000000
ORDER BY students.camID; -- 6.81 6.55 6.75 6.31 7.11 6.36 6.55
-- 6.45
别名始终以相同的 EXPLAIN
输出以 35% 的速度 运行。见下文。因此相同的 Explain 输出已显示两次,但不会导致相同的性能,而是作为一般线索。
+----+-------------+----------+-------+---------------+------------+---------+----------------------+--------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------+-------+---------------+------------+---------+----------------------+--------+----------------------------------------------+
| 1 | SIMPLE | campus | index | PRIMARY | PRIMARY | 4 | NULL | 20 | Using index; Using temporary; Using filesort |
| 1 | SIMPLE | students | ref | ix_stu_cam | ix_stu_cam | 5 | bigtest.campus.camID | 123766 | Using index |
+----+-------------+----------+-------+---------------+------------+---------+----------------------+--------+----------------------------------------------+
优化器目前似乎更喜欢 having 中的别名,尤其是 DISTINCT.
我今天早些时候在 this question 上出丑了。问题是使用 SQL 服务器,正确答案涉及添加 HAVING
子句。我最初犯的错误是认为 SELECT
语句中的别名可以用在 HAVING
子句中,这在 SQL Server 中是不允许的。我犯了这个错误,因为我假设 SQL 服务器具有与 MySQL 相同的规则,这确实允许在 HAVING
子句中使用别名。
这让我很好奇,我在 Stack Overflow 和其他地方四处寻找,找到一堆 material 解释为什么这些 规则 在两个相应的 RDBMS 上强制执行.但是我在任何地方都找不到 performance 含义的解释 allowing/disallowing HAVING
子句中的别名。
举一个具体的例子,我将复制上述问题中出现的查询:
SELECT students.camID, campus.camName, COUNT(students.stuID) as studentCount
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING COUNT(students.stuID) > 3
ORDER BY studentCount
在 HAVING
子句中使用别名而不是重新指定 COUNT
会对性能产生什么影响?这个问题可以在 MySQL 中直接回答,希望有人能深入了解 SQL 中如果支持 HAVING
子句中的别名会发生什么。
在这种情况下,可以在 MySQL 和 SQL 服务器上标记 SQL 问题的情况很少见,所以享受这阳光下的时刻吧。
我不认为真的有任何性能影响,除非 having
子句中的表达式包含复杂的处理(例如,count(distinct)
或复杂的函数,例如字符串处理长字符串)。
我几乎可以肯定,如果在查询中提到两次,MySQL 将执行两次聚合函数。我不确定 SQL 服务器是否会优化第二个引用,但我猜不会(SQL 服务器有一个很好的优化器,但它不是很好的通用表达式消除)。
接下来的问题是表达式的复杂性。 count()
和 sum()
等简单表达式实际上不会产生太多额外开销——一旦聚合已经完成。复杂的表达式可能开始变得昂贵。
如果您在 SQL 服务器中有一个复杂的表达式,您应该能够通过使用子查询来保证它只被计算一次。
我原以为 SQL 会按 FROM
、WHERE
、GROUP BY
、HAVING
、SELECT
的顺序进行ORDER BY
我不是 MYSQL 专家,但在 MYSQL Documentation 中找到了为什么它是合法的原因。
MySQL 扩展了 GROUP BY 的标准 SQL 使用,因此 select 列表可以引用 GROUP BY 子句中未命名的非聚合列。这意味着前面的查询在 MySQL 中是合法的。 您可以使用此功能通过避免不必要的列排序和分组来获得更好的性能。但是,当 GROUP BY 中未命名的每个非聚合列中的所有值都相同时,这主要有用每组。服务器可以自由地从每个组中选择任何值,因此除非它们相同,否则所选的值是不确定的。此外,添加 ORDER BY 子句不会影响来自每个组的 selection 值。结果集排序发生在选择值之后,ORDER BY 不影响服务器在每个组中选择的值。
类似的 MySQL 扩展适用于 HAVING 子句 。在标准 SQL 中,查询不能引用 HAVING 子句中未在 GROUP BY 子句中命名的非聚合列。为了简化计算,MySQL 扩展允许引用此类列。此扩展假定未分组的列具有相同的分组值。否则,结果不确定。
关于性能影响,我假设别名 having 会比非别名 having 慢,因为必须在所有执行后应用过滤器。我会等待专家发表评论。
狭隘地关注特定查询,并在下面加载示例数据。这确实解决了其他一些查询,例如其他人提到的 count(distinct ...)
。
alias in the HAVING
似乎略优于或相当优于其替代方案(取决于查询)。
这使用了一个预先存在的 table,其中包含大约 500 万行,通过我的这个
结果结构:
CREATE TABLE `ratings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`thing` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5046214 DEFAULT CHARSET=utf8;
而是改用 INNODB。由于 运行ge 保留插入,创建预期的 INNODB 间隙异常。只是说,但没有区别。 470 万行。
修改 table 以接近 Tim 的假定架构。
rename table ratings to students; -- not exactly instanteous (a COPY)
alter table students add column camId int; -- get it near Tim's schema
-- don't add the `camId` index yet
以下内容需要一段时间。 运行 一次又一次地分块进行,否则您的连接可能会超时。超时是由于更新语句中没有 LIMIT 子句的 500 万行。请注意,我们 有一个 LIMIT 子句。
所以我们要进行 50 万行迭代。将列设置为 运行dom 数字在 1 到 20 之间
update students set camId=floor(rand()*20+1) where camId is null limit 500000; -- well that took a while (no surprise)
保持运行以上直到没有camId
为空。
我运行喜欢10次(整个过程需要7到10分钟)
select camId,count(*) from students
group by camId order by 1 ;
1 235641
2 236060
3 236249
4 235736
5 236333
6 235540
7 235870
8 236815
9 235950
10 235594
11 236504
12 236483
13 235656
14 236264
15 236050
16 236176
17 236097
18 235239
19 235556
20 234779
select count(*) from students;
-- 4.7 Million rows
创建一个有用的索引(当然是在插入之后)。
create index `ix_stu_cam` on students(camId); -- takes 45 seconds
ANALYZE TABLE students; -- update the stats: http://dev.mysql.com/doc/refman/5.7/en/analyze-table.html
-- the above is fine, takes 1 second
创建校园table。
create table campus
( camID int auto_increment primary key,
camName varchar(100) not null
);
insert campus(camName) values
('one'),('2'),('3'),('4'),('5'),
('6'),('7'),('8'),('9'),('ten'),
('etc'),('etc'),('etc'),('etc'),('etc'),
('etc'),('etc'),('etc'),('etc'),('twenty');
-- ok 20 of them
运行 两个查询:
SELECT students.camID, campus.camName, COUNT(students.id) as studentCount
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING COUNT(students.id) > 3
ORDER BY studentCount;
-- run it many many times, back to back, 5.50 seconds, 20 rows of output
和
SELECT students.camID, campus.camName, COUNT(students.id) as studentCount
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING studentCount > 3
ORDER BY studentCount;
-- run it many many times, back to back, 5.50 seconds, 20 rows of output
所以时间是一样的。 运行各十几次。
EXPLAIN
两者的输出相同
+----+-------------+----------+------+---------------+------------+---------+----------------------+--------+---------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------+------+---------------+------------+---------+----------------------+--------+---------------------------------+
| 1 | SIMPLE | campus | ALL | PRIMARY | NULL | NULL | NULL | 20 | Using temporary; Using filesort |
| 1 | SIMPLE | students | ref | ix_stu_cam | ix_stu_cam | 5 | bigtest.campus.camID | 123766 | Using index |
+----+-------------+----------+------+---------------+------------+---------+----------------------+--------+---------------------------------+
使用 AVG() 函数,通过以下两个查询中 having
中的别名(具有相同的 EXPLAIN
输出),我的性能提高了大约 12%。
SELECT students.camID, campus.camName, avg(students.id) as studentAvg
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING avg(students.id) > 2200000
ORDER BY students.camID;
-- avg time 7.5
explain
SELECT students.camID, campus.camName, avg(students.id) as studentAvg
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID, campus.camName
HAVING studentAvg > 2200000
ORDER BY students.camID;
-- avg time 6.5
最后,DISTINCT
:
SELECT students.camID, count(distinct students.id) as studentDistinct
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID
HAVING count(distinct students.id) > 1000000
ORDER BY students.camID; -- 10.6 10.84 12.1 11.49 10.1 9.97 10.27 11.53 9.84 9.98
-- 9.9
SELECT students.camID, count(distinct students.id) as studentDistinct
FROM students
JOIN campus
ON campus.camID = students.camID
GROUP BY students.camID
HAVING studentDistinct > 1000000
ORDER BY students.camID; -- 6.81 6.55 6.75 6.31 7.11 6.36 6.55
-- 6.45
别名始终以相同的 EXPLAIN
输出以 35% 的速度 运行。见下文。因此相同的 Explain 输出已显示两次,但不会导致相同的性能,而是作为一般线索。
+----+-------------+----------+-------+---------------+------------+---------+----------------------+--------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------+-------+---------------+------------+---------+----------------------+--------+----------------------------------------------+
| 1 | SIMPLE | campus | index | PRIMARY | PRIMARY | 4 | NULL | 20 | Using index; Using temporary; Using filesort |
| 1 | SIMPLE | students | ref | ix_stu_cam | ix_stu_cam | 5 | bigtest.campus.camID | 123766 | Using index |
+----+-------------+----------+-------+---------------+------------+---------+----------------------+--------+----------------------------------------------+
优化器目前似乎更喜欢 having 中的别名,尤其是 DISTINCT.