SELECT COUNT(*) AS player_count, AVG(height) AS avg_height, MAX(height) AS max_height, MIN(height) AS min_height FROM player;
1. COUNT(*) 与 COUNT(column) 的区别
1 2
SELECTCOUNT(*) FROM player;
统计的是结果集行数。
而:
1 2
SELECTCOUNT(height) FROM player;
会忽略 height IS NULL 的数据。
这两个 SQL 语义不同,不能因为看起来都叫 COUNT 就互相替换。
2. GROUP BY:按维度分组
统计每支球队的球员数量:
1 2 3 4 5
SELECT team_id, COUNT(*) AS player_count FROM player GROUPBY team_id;
多个字段联合分组:
1 2 3 4 5 6
SELECT team_id, height, COUNT(*) AS player_count FROM player GROUPBY team_id, height;
3. WHERE 与 HAVING 到底差在哪里
一句话记住:
1 2
WHERE 过滤数据行 HAVING 过滤分组后的结果
例如:先筛选身高大于 1.90 的球员,再按球队统计,只保留球员数大于 5 的球队:
1 2 3 4 5 6 7 8 9
SELECT team_id, COUNT(*) AS player_count, AVG(height) AS avg_height FROM player WHERE height >1.90 GROUPBY team_id HAVINGCOUNT(*) >5 ORDERBY player_count DESC;
这里的处理过程是:
1 2 3 4 5
WHERE 先减少明细数据 → GROUP BY 分组 → 聚集函数计算 → HAVING 过滤统计结果 → ORDER BY 排序
如果把 COUNT(*) > 5 写进 WHERE,逻辑阶段就错了,因为 WHERE 执行时分组统计结果还没有形成。
七、理解 SELECT 的逻辑执行顺序
写 SQL 时,我们习惯的语法顺序是:
1 2 3 4 5 6 7
SELECT ... FROM ... WHERE ... GROUPBY ... HAVING ... ORDERBY ... LIMIT ...;
但理解查询时,更有价值的是下面这个逻辑顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
FROM ↓ WHERE ↓ GROUP BY ↓ 聚集计算 / HAVING ↓ SELECT ↓ DISTINCT ↓ ORDER BY ↓ LIMIT
这能解释大量看似奇怪的问题。
比如为什么 WHERE 里不能直接过滤聚集结果?
因为:
1
WHERE 执行时,GROUP BY 和聚集计算还没发生。
为什么 HAVING 可以?
因为它处于分组和聚集之后。
理解执行顺序以后,SQL 就不再是一堆关键字,而是一条数据处理流水线。
八、子查询:把“查询结果”继续当数据使用
子查询就是嵌套在另一个 SQL 中的查询。
1. 非关联子查询
查询最高的球员:
1 2 3 4 5 6
SELECT player_name, height FROM player WHERE height = ( SELECTMAX(height) FROM player );
内部查询:
1
SELECTMAX(height) FROM player
可以独立执行一次,得到一个固定结果,然后外层查询再使用它。
2. 关联子查询
如果想查询“每支球队中,高于本队平均身高的球员”:
1 2 3 4 5 6 7 8 9 10
SELECT p.player_name, p.team_id, p.height FROM player AS p WHERE p.height > ( SELECTAVG(p2.height) FROM player AS p2 WHERE p2.team_id = p.team_id );
内部查询依赖外部查询当前行的 team_id,因此它属于关联子查询。
3. EXISTS / NOT EXISTS
查询至少存在一条比赛成绩记录的球员:
1 2 3 4 5 6 7
SELECT p.player_id, p.player_name FROM player AS p WHEREEXISTS ( SELECT1 FROM player_score AS ps WHERE ps.player_id = p.player_id );
查询从未出现过比赛记录的球员:
1 2 3 4 5 6 7
SELECT p.player_id, p.player_name FROM player AS p WHERENOTEXISTS ( SELECT1 FROM player_score AS ps WHERE ps.player_id = p.player_id );
EXISTS 关心的是:子查询有没有返回记录,而不是返回了什么字段。
4. IN、ANY、ALL
IN 用于集合包含判断:
1 2 3 4 5 6
SELECT player_id, player_name FROM player WHERE player_id IN ( SELECT player_id FROM player_score );
SELECT ... FROM table1 JOIN table2 ON ... JOIN table3 ON ...;
相比把所有表都堆在 FROM 后,再把连接条件和过滤条件一起塞进 WHERE,这种写法更容易阅读和维护。
1. CROSS JOIN:笛卡尔积
1 2 3
SELECT* FROM player CROSSJOIN team;
如果 player 有 37 行,team 有 3 行,理论上会得到:
1
37 × 3 = 111 行
笛卡尔积本身并不是错误,但业务查询中出现“行数突然爆炸”,第一件事就是检查是不是漏了连接条件。
2. INNER JOIN:只保留匹配数据
1 2 3 4 5 6 7
SELECT p.player_name, p.height, t.team_name FROM player AS p JOIN team AS t ON p.team_id = t.team_id;
JOIN 不写类型时,通常表示 INNER JOIN。
3. LEFT JOIN:左表全部保留
1 2 3 4 5 6
SELECT t.team_name, p.player_name FROM team AS t LEFTJOIN player AS p ON t.team_id = p.team_id;
即使某支球队没有球员记录,球队本身仍然会出现在结果中,对应的球员字段为 NULL。
这类写法特别适合:
查“所有主体 + 可能存在的明细”;
找没有关联数据的记录;
报表中不能丢失主维度的场景。
例如找“没有球员数据的球队”:
1 2 3 4 5
SELECT t.team_id, t.team_name FROM team AS t LEFTJOIN player AS p ON t.team_id = p.team_id WHERE p.player_id ISNULL;
4. RIGHT JOIN
逻辑上与 LEFT JOIN 相反:保留右表全部记录。
1 2 3 4
SELECT ... FROM player AS p RIGHTJOIN team AS t ON p.team_id = t.team_id;
实际工程中,如果团队统一使用 LEFT JOIN,很多 RIGHT JOIN 都可以通过交换表位置改写,这样代码风格更统一。
5. FULL OUTER JOIN
标准 SQL 中还有全外连接,用于同时保留左右两边未匹配的数据。
但它的支持情况与具体 DBMS 有关,所以不要默认所有数据库都能直接执行:
1 2 3 4
SELECT ... FROM table_a FULLOUTERJOIN table_b ON table_a.id = table_b.id;
6. 非等值 JOIN
连接条件并不一定必须使用等号。
例如用身高级别表 height_grades 给球员匹配等级:
1 2 3 4 5 6 7
SELECT p.player_name, p.height, h.height_level FROM player AS p JOIN height_grades AS h ON p.height BETWEEN h.height_lowest AND h.height_highest;
这就是典型的区间连接。
7. 自连接
同一张表也可以和自己 JOIN。
例如找出所有比某个球员更高的人:
1 2 3 4 5 6 7
SELECT b.player_name, b.height FROM player AS a JOIN player AS b ON b.height > a.height WHERE a.player_name ='Blake Griffin';
本质上只是把同一张表起两个不同的别名,然后把它们当作两份数据参与连接。
8. NATURAL JOIN、USING 与 ON
SQL99 还提供:
1 2 3
NATURALJOIN JOIN ... USING (...) JOIN ... ON ...
虽然 NATURAL JOIN 和 USING 可以缩短语句,但大型业务 SQL 更常见的做法仍然是把连接条件明确写出来:
1 2
JOIN team AS t ON p.team_id = t.team_id
这样阅读代码时不需要猜“到底使用了哪个同名字段”。
十、把 WHERE、GROUP BY、HAVING、JOIN 串起来
看一条更接近真实报表场景的 SQL。
需求:
查询身高大于 1.90 的球员;
关联球队名称;
按球队分组;
只保留球员数不少于 5 的球队;
展示球员数和平均身高;
按球员数从高到低排序;
只取前 10 条。
1 2 3 4 5 6 7 8 9 10 11 12 13
SELECT t.team_id, t.team_name, COUNT(*) AS player_count, ROUND(AVG(p.height), 2) AS avg_height FROM player AS p JOIN team AS t ON p.team_id = t.team_id WHERE p.height >1.90 GROUPBY t.team_id, t.team_name HAVINGCOUNT(*) >=5 ORDERBY player_count DESC LIMIT 10;
这条 SQL 几乎把前面的知识全部串起来了。
按逻辑理解:
1 2 3 4 5 6 7 8 9
1. FROM / JOIN:组装 player + team 2. ON:确定两张表如何关联 3. WHERE:只留下 height > 1.90 的明细 4. GROUP BY:按球队分组 5. COUNT / AVG:计算每组统计值 6. HAVING:过滤球员数不足 5 的组 7. SELECT:输出需要的字段 8. ORDER BY:按人数排序 9. LIMIT:只返回前 10 条