SQL Server按主键分组时为何强制聚合同表列?求更优方案
SQL分组有一条通用规则:若要在SELECT子句中引用某列,要么将其包裹在聚合函数中,要么将其加入GROUP BY子句。我理解这条规则及其原因,但我遇到了一个特殊场景,认为应该存在例外:当多表关联且按其中一个表的主键分组时。
假设有两张表(table_A和table_B)。如果我按table_A的主键(PK)分组,那么table_A的其他列在每个分组内的值必然完全相同。我认为SQL Server应该允许我直接引用table_A的其他列,无需包裹聚合函数。
我期望能正常运行的查询如下:
SELECT AVG(B.score) AS average_score ,A.f_name ,A.l_name FROM table_A A JOIN table_B B ON B.table_A_id = A.id GROUP BY A.id
但实际会收到如下错误:
Column 'table_A.f_name' is invalid in the select list because it is
not contained in either an aggregate function or the GROUP BY clause.
我想到了几种解决办法,但都不尽人意。
SELECT AVG(B.score) AS average_score ,MIN(A.f_name) AS first_name ,MIN(A.l_name) AS last_name FROM table_A A JOIN table_B B ON B.table_A_id = A.id GROUP BY A.id
查询结果:
average_score first_name last_name ------------- ---------- ---------- 12 Bob Ross 11 Ricky Bobby 12 Rick Ross
我不喜欢这种方式,因为它模糊了代码的实际意图:我们并非真的要取最小值。实际上无论选择哪种聚合函数,MIN()和MAX()都会返回相同结果。
SELECT AVG(B.score) AS average_score ,MAX(A.f_name) AS first_name ,MAX(A.l_name) AS last_name FROM table_A A JOIN table_B B ON B.table_A_id = A.id GROUP BY A.id
查询结果:
average_score first_name last_name ------------- ---------- ---------- 12 Bob Ross 11 Ricky Bobby 12 Rick Ross
这说明table_A的所有列在分组内值完全相同,因此我认为不应强制我对这些列使用聚合函数,毕竟已经按主键分组了。
这里我们将SELECT子句中未用聚合函数的所有列都加入GROUP BY。
SELECT AVG(B.score) AS average_score ,A.f_name ,A.l_name FROM table_A A JOIN table_B B ON B.table_A_id = A.id GROUP BY A.f_name ,A.l_name ,A.id
查询结果:
average_score f_name l_name ------------- ---------- ---------- 12 Bob Ross 11 Ricky Bobby 12 Rick Ross
同样,这种方式模糊了代码意图。我们的目标是让每个用户自成一组,但代码看起来像是先按firstName分组,再按lastName细分。由于可能存在重名用户,仍需加入主键确保每组仅一人,但如果直接按主键分组本来可以一步到位。这种写法在语法上不够简洁(不确定性能是否受影响,但可读性差)。
这里我们在SELECT子句中使用子查询而非关联查询。
SELECT AVG(B.score) AS average_score ,(SELECT A.f_name FROM table_A A WHERE A.id = B.table_A_id) AS first_name ,(SELECT A.L_name FROM table_A A WHERE A.id = B.table_A_id) AS last_name FROM table_B B GROUP BY B.table_A_id
查询结果:
average_score first_name last_name ------------- ---------- ---------- 12 Bob Ross 11 Ricky Bobby 12 Rick Ross
我也不喜欢这种方式,因为每要返回table_A的一列就需要重复几乎相同的子查询。table_A和table_B显然更适合用关联查询。当需要返回更多数据时,问题会更严重,尤其是需要通过table_A关联其他表时。
SELECT AVG(B.score) AS average_score ,(SELECT A.f_name FROM table_A A WHERE A.id = B.table_A_id) AS first_name ,(SELECT A.L_name FROM table_A A WHERE A.id = B.table_A_id) AS last_name ,(SELECT C.team FROM table_C C WHERE C.id = (SELECT A.table_C_id FROM table_A A WHERE A.id = B.table_A_id)) AS team_name FROM table_B B GROUP BY B.table_A_id
查询结果:
average_score first_name last_name team_name ------------- ---------- ---------- ---------- 12 Bob Ross Team A 11 Ricky Bobby Team A 12 Rick Ross Team B
以下是使用方案1实现相同逻辑的查询:
SELECT AVG(B.score) AS average_score ,MIN(A.f_name) AS first_name ,MIN(A.l_name) AS last_name ,MIN(C.team) AS team_name FROM table_A A JOIN table_B B ON B.table_A_id = A.id JOIN table_C C ON C.id = A.table_C_id GROUP BY A.id
查询结果:
average_score first_name last_name team_name ------------- ---------- ---------- ---------- 12 Bob Ross Team A 11 Ricky Bobby Team A 12 Rick Ross Team B
大家是否也觉得这种情况很棘手?有没有比我想到的更优雅的解决办法?其他数据库也存在这个问题吗?
内容的提问来源于stack exchange,提问作者illchangethislater

