DQL 数据查询语言:把 SELECT 的每个子句讲透
以 players 排行榜表讲透 SELECT:WHERE 筛选、LIKE 模糊匹配、IN/BETWEEN、IS NULL、ORDER BY 多列排序、LIMIT 分页、DISTINCT 去重与 GROUP BY 聚合入门,附 1054/1055 真实报错与执行顺序心法。
前置知识
- DML 数据操作语言:会 INSERT/UPDATE/DELETE,知道「写完要用查询验证」;
- 统一案例
players排行榜表的建表脚本在 SQL 数据操作与查询 第 2 节,没建表的先去复制执行。
SELECT 你在 110 里零散用过几次,本文把它拆开讲全:每个子句解决什么、按什么顺序执行、写错报什么。查数据不会毁数据,可以放开手试——这是 DQL(Data Query Language,数据查询语言)和 DML 最大的区别。
学习目标
读完本文你将能够:
- 用 WHERE 的比较、IN、BETWEEN、LIKE、IS NULL 五类条件精确筛行,避开
= NULL与括号优先级两个陷阱; - 用 ORDER BY 多列排序做出名次稳定的排行榜,用 LIMIT/OFFSET 写出任意一页的分页;
- 用 DISTINCT 去重,用 COUNT/AVG/SUM/MAX/MIN 做统计,写出最小可用的 GROUP BY;
- 画出 SELECT 子句的执行顺序,解释「WHERE 里为什么不能用别名」;
- 修复
ERROR 1054、ERROR 1055两种高频报错。
预计 60 到 75 分钟,全程动手。
1. 你现在要解决什么问题
排行榜后台今天被产品提了一串需求:
- 「等级 6 或 10 的玩家名单发我」;
- 「排行榜页面,同分的按注册先后排」;
- 「列表页一页 3 名,给我第 3 页」;
- 「统计每个等级有多少人、全表平均分多少」;
- 「找出还没填昵称的玩家,推送去提醒」。
每一条都是一条 SELECT——你缺的不是语法,是把每个子句用在刀刃上。往下走。
2. 准备战场:把数据灌成标准状态
先灌数据(建表脚本见 110 第 2 节;表里有旧数据先 TRUNCATE,用法见 100 第 5 节):
INSERT INTO players (name, score, level) VALUES
('小明', 1250, 8),
('阿黄', 980, 6),
('Rain', 2410, 15),
('Nova', 760, 5),
('老K', 1520, 10),
('Tiny', 410, 3),
('南风', 980, 6),
('Zed', 1520, 10);产品临时加了「自定义昵称」需求,补一列(ALTER 是 DDL,先混个眼熟,080/090 讲透;报 Duplicate column name 说明列已存在,跳过):
ALTER TABLE players ADD COLUMN nickname VARCHAR(20) DEFAULT NULL;
UPDATE players SET nickname = '闪电侠' WHERE name = '小明';
UPDATE players SET nickname = '雨神' WHERE name = 'Rain';现在表里有 8 名玩家,id 1 到 8,两人有昵称——本文的标准状态。
3. SELECT 的骨架:列、星号与别名
只取需要的列,别每次 SELECT *:
SELECT name, score FROM players;预期输出 8 行:小明 1250 → 阿黄 980 → Rain 2410 → Nova 760 → 老K 1520 → Tiny 410 → 南风 980 → Zed 1520。行的先后没有保证,别依赖它。
SELECT * 调试图快可以,写进项目是坏味道:多拉无用的列浪费网络与内存,也看不出你要用哪几列。起别名用 AS:SELECT name AS 玩家, score AS 积分 FROM players WHERE id <= 3; 预期三行,表头变成「玩家」「积分」。AS 可省略;中文别名合法,跟团队约定走。
4. WHERE:把全表筛成你要的行
4.1 比较与逻辑
SELECT name, score FROM players WHERE score >= 1520;预期:Rain 2410、老K 1520、Zed 1520,共 3 行。多条件用 AND/OR 连接,小心优先级——AND 比 OR 结合得更紧:
SELECT name, score, level FROM players WHERE level = 6 OR level = 10 AND score > 1400;你以为的语义是「(6 级或 10 级) 且分数超 1400」,引擎实际读成「6 级,或(10 级且超 1400)」——返回 4 行,把两个 980 分的 6 级玩家也带进来了。想要前者必须加括号:WHERE (level = 6 OR level = 10) AND score > 1400;,返回 2 行(老K、Zed)。
4.2 IN 与 BETWEEN
同一列取多个离散值,用 IN 代替一串 OR:
SELECT name, level FROM players WHERE level IN (6, 10);预期:阿黄、老K、南风、Zed 共 4 行,等价 level = 6 OR level = 10 但更短。连续区间用 BETWEEN,含两端:
SELECT name, score FROM players WHERE score BETWEEN 980 AND 1520;预期 5 行:小明 1250、阿黄 980、老K 1520、南风 980、Zed 1520,等价于 >= 980 AND <= 1520。
4.3 LIKE:模糊匹配的两个通配符
% 匹配任意多个字符,_ 恰好一个字符:
SELECT name FROM players WHERE name LIKE 'R%'; -- Rain:R 开头
SELECT name FROM players WHERE name LIKE '__'; -- 恰好两个字符:小明、阿黄、老K、南风两条查询预期分别返回 1 行和 4 行。LIKE '%词%' 会让索引失效,大表慎用。
4.4 IS NULL:NULL 的专属判断
查没填昵称的玩家不能用 nickname = NULL——NULL 与任何值比较结果都是「未知」而非「真」,WHERE 只放行「真」,那条条件永远空手而归。NULL 有专属语法:
SELECT name FROM players WHERE nickname IS NULL; -- 6 行:没填昵称的
SELECT name, nickname FROM players WHERE nickname IS NOT NULL; -- 2 行:闪电侠、雨神5. 排序与分页:ORDER BY 与 LIMIT
5.1 多列排序做出稳定的名次
SELECT name, score FROM players ORDER BY score DESC, id ASC;预期 8 行,依次是:Rain 2410 → 老K 1520 → Zed 1520 → 小明 1250 → 阿黄 980 → 南风 980 → Nova 760 → Tiny 410(DESC 降序、ASC 升序,缺省 ASC)。
重点在两个同分组:老K 与 Zed 同分 1520,阿黄与南风同分 980。只按 score 排序时,同分行先后没有保证——今天老K 在前,明天可能换过来。要名次稳定必须加第二排序键,这里用「先注册靠前」(id ASC)。
5.2 LIMIT 与分页
SELECT name, score FROM players ORDER BY score DESC, id ASC LIMIT 3; -- 前三名
SELECT name, score FROM players ORDER BY score DESC, id ASC LIMIT 3 OFFSET 2; -- 跳过 2 名取 3 名第一条预期:Rain、老K、Zed;第二条预期:Zed、小明、阿黄。分页公式「第 n 页每页 k 条:LIMIT k OFFSET (n-1)*k」——第 3 页每页 3 条是 LIMIT 3 OFFSET 6,预期返回南风、Nova,最后一页不满是正常现象。
容易翻车的写法:LIMIT 2, 3 与 LIMIT 3 OFFSET 2 等价,但逗号前是偏移量、后才是条数,顺序相反,团队统一用 OFFSET 更安全。
6. 去重与聚合:DISTINCT 与五个聚合函数
DISTINCT 把结果里重复的值压成一个:
SELECT DISTINCT level FROM players ORDER BY level;预期:8 行进、6 行出——3 5 6 8 10 15 各一行,重复的 6 和 10 只留一份。
聚合函数把「很多行」算成「一个数」:
SELECT COUNT(*) AS total, AVG(score) AS avg_score, MAX(score) AS max_score,
MIN(score) AS min_score, SUM(score) AS total_score
FROM players;预期一行:total 8、avg_score 1228.7500、max_score 2410、min_score 410、total_score 9830。COUNT(*) 数所有行,COUNT(nickname) 只数非 NULL 的行——本文数据里是 2。「每个等级有多少人」这种分组统计,轮到 GROUP BY。
7. GROUP BY 最小入门
SELECT level, COUNT(*) AS player_count
FROM players
GROUP BY level
ORDER BY player_count DESC, level ASC;预期输出:
+-------+--------------+
| level | player_count |
+-------+--------------+
| 6 | 2 |
| 10 | 2 |
| 3 | 1 |
| 5 | 1 |
| 8 | 1 |
| 15 | 1 |
+-------+--------------+
6 rows in set (0.00 sec)GROUP BY level 把 8 行按等级分成 6 组,每组输出一行结论。规矩:SELECT 里只能出现分组列和聚合结果,多写一列就会撞上 8.0 默认开启的 only_full_group_by 模式:
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'test.players.name' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by「每个等级分数最高的玩家是谁」不是多写一列能解决的,要等子查询与窗口函数(130 篇起)。分组后想「只看人数超过 1 的等级」,不能用 WHERE,要用搭档 HAVING——同样 130 篇讲透。
8. 核心概念:SELECT 的执行顺序
书写顺序和执行顺序不一样,这条心法能解释大半怪现象:
书写:SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ...
执行:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMITSELECT(别名在这里诞生)排在 WHERE 之后,所以 WHERE 里用不上 SELECT 的别名:
SELECT name, score * 2 AS double_score FROM players WHERE double_score > 4000;ERROR 1054 (42S22): Unknown column 'double_score' in 'where clause'修法是把表达式抄进 WHERE:WHERE score * 2 > 4000。而 ORDER BY 排在 SELECT 之后,ORDER BY double_score 合法。这条顺序也是本章总答案:WHERE 先筛行,GROUP BY 再分组。
9. 常见错误与调试实录
错误一:ERROR 1054(列不存在)。来源一是拼写错(ORDER BY scor → Unknown column 'scor' in 'order clause',110 见过 field list 版本),来源二是第 8 节的「WHERE 里用别名」。读报错三步:看错误码,看引号里哪个名字不存在,用 DESC players; 核对。
错误二:ERROR 1055(GROUP BY 违规)。第 7 节原文已给出。只需读两处:Expression #N 指出 SELECT 里第几个表达式违规,only_full_group_by 点明是模式限制。修法:把该列拿走,或用聚合函数包住。
错误三:不报错但结果不对——最危险。= NULL 永远空结果、同分名次抖动、LIMIT 2, 3 偏移当条数,全都不报错。防线只有一个:跑完先问「行数和内容符合预期吗」。
10. 修改实验
- 把 5.2 节的查询改成「第 4 到第 6 名」,先预测是哪三名,运行验证;
- 给排行榜加第三排序键
level DESC,观察同分行有没有变化; - 把 6 节的 DISTINCT level 换成 DISTINCT score,先预测行数再验证;
- 复现一次
ERROR 1054:把某条查询的列名删掉一个字母,读报错、修好。
11. 实际项目中的使用场景
- 排行榜、热榜:ORDER BY + LIMIT,配稳定排序键防名次抖动;
- 后台列表页:WHERE 筛选 + LIMIT/OFFSET 分页(大表深分页的优化在索引篇);
- 数据看板:COUNT/AVG/SUM 配 GROUP BY 出报表;
- 去重与排查:DISTINCT/GROUP BY 去重导出,IS NULL 与 LIKE 找脏数据。
12. 小练习
预测题(不执行,先写答案):表处于第 2 节的标准状态,执行
SELECT name, score FROM players
WHERE score BETWEEN 900 AND 1600 AND nickname IS NULL
ORDER BY score DESC, id ASC
LIMIT 2 OFFSET 1;输出哪几行、顺序如何?写完答案再运行验证。
修改题:把排行榜查询改成「每页 3 名的第 2 页」,并保证同分玩家按 id 升序排列。
修 Bug 题:第一条不报错但查不出人,第二条报 ERROR 1054。分别找出原因并修复:
SELECT name, score FROM players WHERE nickname = NULL;
SELECT name, score FROM players WHERE score > 1500 ORDER BY scor DESC;挑战题(不看提示自己写):查出「分数在 1000 到 2000 之间、按分数降序(同分按 id 升序)」的玩家,取第 2 页(每页 2 条),列名用中文别名。验收:结果恰好一行,玩家是小明。
13. 与之前和之后的知识的关系
- 往前:SQL 数据操作与查询 里用过 ORDER BY/LIMIT/聚合的零散片段,本文给了完整理论与执行顺序;
- 往后:SQL 函数与高级查询 展开 HAVING、内置函数与子查询,把「分组后过滤」「每组取最高」讲透;跨表见 多表连接;
- 更远:LIMIT/OFFSET 深分页的性能问题,等索引与执行计划篇再收拾。
14. 官方文档
- SELECT 语法(含 ORDER BY/LIMIT):https://dev.mysql.com/doc/refman/8.4/en/select.html
- 聚合函数(GROUP BY 修饰):https://dev.mysql.com/doc/refman/8.4/en/aggregate-functions.html
- 字符串比较与 LIKE:https://dev.mysql.com/doc/refman/8.4/en/string-comparison-functions.html
- LIMIT 查询优化:https://dev.mysql.com/doc/refman/8.4/en/limit-optimization.html
- 服务端错误码总表:https://dev.mysql.com/doc/mysql-errors/8.4/en/server-error-reference.html
15. 自我检查
- 能不查资料写出 IN、BETWEEN、LIKE、IS NULL 四类条件,并说出 BETWEEN 是否含两端;
- 能解释为什么同分的排行榜必须加第二排序键;
- 能画出 SELECT 子句执行顺序,解释 WHERE 里用别名为什么报 1054;
- 能写出按等级统计人数的 GROUP BY,说出 SELECT 里能放哪些列;
- 拿到 1054 与 1055,各自知道第一步看哪里。
本章总结
SELECT 是一条流水线:FROM 备好行,WHERE 筛行(不能碰别名),GROUP BY 分组聚合,HAVING 过滤组,SELECT 产出别名,ORDER BY 定序(同分必须有第二排序键),LIMIT 切页。DISTINCT 去重、五个聚合函数出统计、GROUP BY 出报表——开头五个产品需求全部有了答案。报错两兄弟:1054 列不存在,1055 分组违规。
下一步
进入 SQL 函数与高级查询:HAVING、内置函数与子查询登场,把「每个等级分数最高的玩家是谁」这类需求真正解决。