允许在 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 会按 FROMWHEREGROUP BYHAVINGSELECT 的顺序进行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.