
MySQL 单表查询这东西说难真不难说简单很多干了三五年的同事偶尔也会在某个细节上卡一下。尤其在业务里频繁碰数据的人不管是后端开发、数据分析还是运维偶尔要捞数据单表查询都是第一道绕不过去的门槛。很多刚入门的朋友装好 MySQL 之后对着黑乎乎的终端发愣不知道从哪下手或者在网上东看一篇西看一篇结果连 SELECT 和 WHERE 都还没搞清楚就被各种 JOIN、子查询劝退了。这篇文章我就把自己当年踩过的坑和常用的套路整理出来从最基础的 SELECT 语法讲起到条件过滤、排序、分页、聚合分组最后用一个完整的实战案例把知识点串起来。整个过程不需要任何前置经验跟着一步步操作就能跑通。内容安排上我会先用最简单的方式把环境跑起来再逐层递进讲查询的各个知识点每一段都会配合可直接执行的 SQL 和输出结果方便你对照验证。文章风格偏实操尽量不堆概念该拆原理的地方会用大白话解释保证读下来不费劲。1. 环境准备与数据初始化1.1 本地安装 MySQL 的两种路线想练查询首先得有个能跑的环境。针对小白我推荐两条路线按自己的系统选一条就行。第一条是 Windows 下的可视化安装。去 MySQL 官网下载 MySQL Community Server 的 ZIP 包或 MSI 安装包双击安装时选 Server Only端口默认 3306认证方式建议选 “Use Legacy Authentication”MySQL 8.0 默认是 caching_sha2_password很多老客户端和图形工具连接会报错选 Legacy 能省掉后续一堆麻烦。安装过程中会要求设置 root 密码自己设一个简单好记的比如 root123本地学习足够。第二条是 Docker 方式适合不想污染本机环境的同学。提前装好 Docker Desktop然后执行docker run -d --name mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORDroot123 mysql:8.0这个命令会拉取 MySQL 8.0 镜像并在容器里启动3306 端口映射到宿主机。实测下来Docker 方式对环境最友好删除容器后所有数据清空练习阶段特别方便。要注意的是不管是哪种方式装完先确认服务已启动。Windows 下可以打开服务管理器找 MySQL 服务Docker 下执行docker ps看容器状态。连接数据库我建议先用命令行客户端。Windows 下打开 cmd 或 PowerShell进入 MySQL 安装目录的 bin 文件夹执行mysql -uroot -p输入密码后就会看到mysql提示符。这一步别嫌麻烦命令行操作虽然看起来吓人但它是理解 SQL 执行逻辑的最好工具后面所有练习我都会基于命令行演示。1.2 创建练习库与测试表环境跑通之后先建一个专门的练习库别把测试数据堆在系统库里。这里我建一个名为demo的数据库创建一张员工表用来承载后续所有查询案例。CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARSET utf8mb4; USE demo; CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 员工编号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) COMMENT 性别M/F, age INT COMMENT 年龄, department VARCHAR(50) COMMENT 所属部门, salary DECIMAL(10,2) COMMENT 月薪, hire_date DATE COMMENT 入职日期 ) ENGINEInnoDB;建表时用utf8mb4字符集很重要否则后续插入中文姓名会出现乱码或报错。字段类型我特意选了最常用的几种整数、字符串、小数、日期这样后面讲查询时每个类型都能覆盖到。接着插入一批测试数据INSERT INTO employee (name, gender, age, department, salary, hire_date) VALUES (张伟, M, 28, 技术部, 15000.00, 2020-03-15), (李娜, F, 32, 产品部, 18000.00, 2018-07-01), (王强, M, 35, 技术部, 22000.00, 2016-11-20), (刘敏, F, 26, 运营部, 12000.00, 2021-05-10), (陈杰, M, 30, 技术部, 16000.00, 2019-08-22), (赵丽, F, 29, 产品部, 17000.00, 2020-01-06), (孙勇, M, 41, 市场部, 25000.00, 2015-04-18), (周芳, F, 24, 运营部, 11000.00, 2022-02-14), (吴磊, M, 27, 市场部, 13000.00, 2021-09-30), (郑红, F, 33, 技术部, 20000.00, 2017-12-03);插入完成后用SELECT * FROM employee;看一眼如果返回十行数据环境就彻底准备好了。之后所有小节里的例子都是基于这张表来跑。如果你自己想多造点数据可以再执行几次 INSERT 语句数据越多后面讲排序、分页、分组时的效果越直观。2. SELECT 基础查询的核心语法2.1 最简单的查询语句是怎么执行的SELECT 语句是 SQL 世界里使用频率最高的语句没有之一。它的基本逻辑其实特别像“从箱子里挑东西”——指定从哪张表拿数据指定要哪些列再决定是否过滤、排序、分组。MySQL 执行一条 SELECT 时内部会走“FROM - WHERE - GROUP BY - HAVING - ORDER BY - LIMIT”这样的逻辑顺序这和书写顺序不一样理解这一点对排查问题很有帮助。拿我们建的 employee 表为例最简单的查询就是SELECT * FROM employee;这里的*表示返回所有列。日常开发中我建议尽量少用*因为数据量大时它会把不需要的字段也拉出来增加网络传输和内存消耗但练习阶段无所谓怎么方便怎么来。只查指定列的话SELECT name, department, salary FROM employee;这条语句返回三列相当于只挑出了姓名、部门、薪水这三样东西。你可以这样理解SELECT 后面跟的是“投影”FROM 后面跟的是“数据源”投影决定了你看哪些列数据源决定了从哪张表拿。MySQL 还支持在查询时给列起别名用AS关键字也可以直接省略 AS 用空格分隔SELECT name AS 姓名, department AS 部门, salary AS 月薪 FROM employee;起别名的意义有两个一是让结果集表头更可读比如在导出 Excel 时中文表头更友好二是在后面讲聚合函数、子查询时别名是必须的因为计算结果本身没有名字不取别名你没法引用它。2.2 去重、拼接与字面量查询除了直接查列SELECT 还有很多实用的小技巧这里挑几个新手高频用到的。去重用DISTINCT它的作用是去掉结果集中的重复行。我们想看看公司有哪些部门直接查 department 会返回 10 行里面有很多重复值SELECT DISTINCT department FROM employee;这条语句会返回技术部、产品部、运营部、市场部四行。注意DISTINCT 作用于整行而不是单独一列。如果写SELECT DISTINCT department, gender FROM employee它去重的是“部门性别”的组合不是只去重部门这是一个常见的理解误区。字符串拼接用CONCAT函数。比如想把姓名和部门拼在一列里展示SELECT CONCAT(name, - , department) AS info FROM employee;执行后你会看到类似“张伟 - 技术部”这样的内容。函数在 SELECT 里非常常用但新手阶段不用强记很多先把 CONCAT、NOW、IFNULL 这几个常用的记住就行。还有一种不带 FROM 的 SELECT直接计算字面量SELECT 1 1 AS result;执行结果是 2。虽然这个能力看起来好像没用但实际排查问题时很好用——比如你怀疑 MySQL 连接有问题先执行个SELECT 1能返回结果就说明数据库连接是通的这招在排查连接池问题时特别实用。3. 条件过滤 WHERE 的进阶用法3.1 比较运算符与逻辑组合没有 WHERE 的 SELECT 就像打开冰箱把所有东西都拿出来而 WHERE 就是你的筛选标准。它是单表查询里最核心、最常写的部分几乎所有实际查询都离不开它。WHERE 的基本语法就是在 SELECT 后加过滤条件MySQL 逐行判断条件真假真的留下假的剔除。比如查技术部所有员工SELECT name, department, salary FROM employee WHERE department 技术部;这里用了一个等值比较。除了等值MySQL 还支持、、、、!或。比如查月薪大于 15000 的员工SELECT name, salary FROM employee WHERE salary 15000;多个条件用AND、OR组合。AND 表示同时满足OR 表示满足任意一个即可。查技术部且月薪大于 15000 的SELECT name, department, salary FROM employee WHERE department 技术部 AND salary 15000;这里要特别提醒优先级问题AND 的优先级高于 OR。如果你写WHERE department 技术部 OR department 产品部 AND salary 15000MySQL 会先计算产品部 AND salary 15000再和技术部做 OR结果就是“所有技术部员工 产品部高薪员工”这个结果往往不是你想要的。想明确逻辑关系时用括号包起来SELECT name, department, salary FROM employee WHERE (department 技术部 OR department 产品部) AND salary 15000;3.2 IN、BETWEEN 与模糊匹配 LIKE除了直接比较WHERE 里还有三个高频操作符IN、BETWEEN、LIKE。IN 表示在指定集合内。查技术部和产品部的员工SELECT name, department FROM employee WHERE department IN (技术部, 产品部);这条和上面用 OR 写的结果一样但可读性好很多。IN 后面的集合可以是固定的值也可以是子查询结果这在后续进阶中会用到。BETWEEN 用于范围判断闭区间包含两端。查薪水在 12000 到 18000 之间的员工SELECT name, salary FROM employee WHERE salary BETWEEN 12000 AND 18000;等价写法是salary 12000 AND salary 18000。BETWEEN 看着清爽但注意它只适用于连续范围别拿它去匹配离散集合。另外处理日期时 BETWEEN 也很好用比如SELECT name, hire_date FROM employee WHERE hire_date BETWEEN 2020-01-01 AND 2021-12-31;这里日期字符串会被 MySQL 自动转换为日期类型进行比较。LIKE 用于模糊匹配配合两个通配符使用%表示任意多个字符包括零个_表示任意单个字符。查所有姓张的员工SELECT name FROM employee WHERE name LIKE 张%;查名字为两个字的员工注意中文字符在 utf8mb4 下每个字算一个字符但_匹配的是一个字符拼音和中文都算SELECT name FROM employee WHERE name LIKE __;LIKE 的性能问题要提前有意识如果字段上有索引LIKE 张%这种前缀匹配通常还能走索引但LIKE %张%这种包含匹配很大概率会全表扫描数据量大时会很慢。这在实际工作中是个非常常见的坑。3.3 空值判断的坑SQL 里的NULL和空字符串是两回事。NULL 表示“未知的、不存在的值”空字符串表示“一个长度为 0 的字符串”。在 WHERE 里判断 NULL 不能用等号必须用IS NULL或IS NOT NULL。假设我们表里有几条记录的 gender 是 NULL你可以手动 UPDATE 几条体验一下查性别为空的所有员工SELECT name, gender FROM employee WHERE gender IS NULL;如果你写成WHERE gender NULL结果是查不到任何数据的因为 NULL NULL 的结果不是真也不是假而是“未知”WHERE 只保留条件为真的行。这是个特别经典的新手易错点面试也经常考。顺带一提NULL 在排序时默认排在最前面这在后面讲 ORDER BY 时会再碰到。4. 排序 ORDER BY 与分页 LIMIT4.1 单列排序与多列排序查询结果如果不排序MySQL 返回的顺序是不保证的。虽然小数据量下看着像按插入顺序返回但这不是规范行为。想让自己查询结果可控必须用 ORDER BY。按薪水从高到低排序降序用 DESCSELECT name, department, salary FROM employee ORDER BY salary DESC;升序用 ASC而且 ASC 可以省略因为默认就是升序SELECT name, hire_date FROM employee ORDER BY hire_date ASC;多列排序的写法是按优先级从左到右排列。比如先按部门排序同一部门内再按薪水降序SELECT name, department, salary FROM employee ORDER BY department ASC, salary DESC;这样技术部的几个人会排在一起而且各自部门内部是薪水从高到低。实际业务里这种“先按 A 分组再在组内按 B 排序”的需求非常常见一定要掌握。另外注意ORDER BY 后面可以跟列名、别名也可以跟数字表示第几列比如ORDER BY 3表示按第三列排序但为了可读性建议别用数字。4.2 LIMIT 分页与排序配合LIMIT 用于限制返回行数语法是LIMIT offset, row_count。offset 表示跳过的行数row_count 表示要返回的行数。查薪水最高的前 3 名SELECT name, salary FROM employee ORDER BY salary DESC LIMIT 3;这条只返回前三行等价于LIMIT 0, 3。分页场景下第 2 页每页 3 条就写LIMIT 3, 3表示跳过前 3 条取接下来的 3 条。LIMIT 和 ORDER BY 配合是分页查询的黄金搭档但有两个点要记住。第一LIMIT 的 offset 过大时性能会下降因为 MySQL 需要扫描并丢弃前面的行比如LIMIT 100000, 10就会比较慢这在数据量上来以后是会真实遇到的性能瓶颈。第二如果不加 ORDER BY 就分页那么每一页的数据是不确定的因为记录顺序本身不保证你看到的“第一页”和“第二页”可能是同一批数据的不同排列这在业务上会导致重复或遗漏。所以任何分页查询都必须配排序。4.3 排序中的 NULL 处理刚才提到 NULL 在排序时的位置MySQL 默认升序时 NULL 在最前降序时 NULL 在最后。如果你想让 NULL 始终排在最后可以这样写SELECT name, gender FROM employee ORDER BY ISNULL(gender), gender ASC;ISNULL 函数在 gender 为 NULL 时返回 1否则返回 0所以先按这个值排序NULL 的行值为 1就会沉到底部。这个技巧在清洗数据时很实用——你可以先把有效数据看完再决定空值怎么处理。5. 聚合函数与分组统计5.1 五大常用聚合函数单表查询讲到聚合函数就要把视角从“看每一行”切换到“看整体”。聚合函数的特点是输入多行输出一行。常用的有五个COUNT、SUM、AVG、MAX、MIN。COUNT 统计行数最常用的写法有两种SELECT COUNT(*) FROM employee; -- 统计总行数 SELECT COUNT(gender) FROM employee; -- 统计 gender 非空的个数这两者的区别就是COUNT(*)统计所有行COUNT(某列)统计该列非 NULL 的行数。所以如果某列有 NULL两个结果会不一样。这个差异经常被忽略我建议在统计业务数据时先搞清楚到底需要哪个口径。SUM 求和、AVG 求平均、MAX 求最大、MIN 求最小用法都很直接SELECT SUM(salary) AS 总薪资, AVG(salary) AS 平均薪资, MAX(salary) AS 最高薪资, MIN(salary) AS 最低薪资 FROM employee;注意SUM 和 AVG 会自动忽略 NULL 值。如果被统计的列有 NULL结果不会报错但计算口径是“非空值的统计”。另外在计算平均值时AVG(salary) 和 SUM(salary)/COUNT(salary) 结果可能不同因为 COUNT(salary) 不会把 NULL 算进去而如果某行 salary 为 NULLSUM 也把它排除了所以两者结果一致但你要是用 COUNT(*)把 NULL 行了也算进去口径就变了。5.2 GROUP BY 分组与 HAVING 过滤分组统计是单表查询的进阶核心。GROUP BY 的作用是把相同值的行合并成一组然后在每组上应用聚合函数。比如按部门统计平均薪资SELECT department, AVG(salary) AS avg_salary FROM employee GROUP BY department;执行后你会看到四个部门各自的平均薪资。这里有个新手必须记住的规则SELECT 后面出现的列要么是 GROUP BY 里的列要么是聚合函数包裹的列不能 SELECT 一个既不在 GROUP BY 里、也不在聚合函数里的裸列。比如下面这条在很多数据库里会直接报错-- 错误示例MySQL 5.7 以后默认会报错 SELECT name, department, AVG(salary) FROM employee GROUP BY department;name 既不在 GROUP BY 里也没被聚合这样查出来的 name 到底是哪个员工的语义上说不通。MySQL 在 5.7.5 之后默认开启了 ONLY_FULL_GROUP_BY 模式这种写法直接给你报错这是好事逼着你写规范的 SQL。更早版本的 MySQL 允许这种写法但返回的 name 是随机的这种不确定性在生产环境里非常危险。分组之后还要过滤用的不是 WHERE 而是 HAVING。WHERE 是在分组前过滤行HAVING 是在分组后过滤组。统计平均薪资大于 15000 的部门SELECT department, AVG(salary) AS avg_salary FROM employee GROUP BY department HAVING avg_salary 15000;这里 HAVING 里可以直接用别名 avg_salary而 WHERE 里不能这样用因为 WHERE 的执行顺序在 SELECT 别名定义之前。执行顺序的差异是理解 WHERE 和 HAVING 区别的关键WHERE 先对原始行做筛选再分组而 HAVING 是在分组和聚合完成之后对组结果做筛选。理论上所有 HAVING 能做的事用子查询也能做但 HAVING 写法更直白。5.3 分组统计的实战套路分组和聚合配合起来能解决很多“统计类”需求。我举一个比较综合的例子统计每个部门的员工数、平均薪资、最高薪资并且只保留员工数大于等于 2 的部门。SELECT department, COUNT(*) AS emp_count, AVG(salary) AS avg_salary, MAX(salary) AS max_salary FROM employee GROUP BY department HAVING COUNT(*) 2 ORDER BY avg_salary DESC;这条语句把本章所有知识点串起来了分组、聚合、HAVING 过滤、ORDER BY 排序。执行顺序是先 FROM employee 拿全表数据再 GROUP BY department 分组对每组执行 COUNT、AVG、MAX然后 HAVING 过滤掉组内人数小于 2 的部门最后按平均薪资降序排列。理解这个执行顺序比死记硬背 SQL 语法重要得多。再补充一个 GROUP BY 结合日期函数的常见场景。按年份统计入职人数SELECT YEAR(hire_date) AS hire_year, COUNT(*) AS cnt FROM employee GROUP BY YEAR(hire_date) ORDER BY hire_year;MySQL 允许在 GROUP BY 里写函数表达式前面的 SELECT 里也要用相同的表达式或别名否则结果解释不清楚。按年、按月、按日统计是数仓和报表场景里的家常便饭date 类函数可以之后单独学但思路在这里先建立起来。6. 单表查询综合实战从需求到 SQL6.1 实战场景还原理论知识讲完来一个完整的实战走一遍。这样才有“从需求到 SQL”的完整链路感。我说一个最常见的业务需求运营想要一份名单2020 年 1 月 1 日之后入职、月薪大于 13000 的男性员工按部门分组统计每个部门的平均薪资和人数人数不少于 2 人的部门才展示最后按平均薪资从高到低排取前 3 条。这个需求看起来长但拆解下来就是清晰的两步先用 WHERE 做基础过滤再用 GROUP BY 分组聚合处理最后排序取前 3。不建议一上来就想着一个大 SQL 搞定先拆需求再逐步拼装这才是写 SQL 的正确工作方式。6.2 逐步拆解与验证第一步先查出入职日期在 2020-01-01 之后、月薪大于 13000、性别为 M 的员工SELECT name, department, salary, hire_date FROM employee WHERE hire_date 2020-01-01 AND salary 13000 AND gender M;这里日期比较直接用字符串也行MySQL 会自动把字符串和 DATE 类型比较时转换。执行后你会看到符合条件的员工列表得到这个中间结果后先确认数据是对的再继续往下写。第二步在过滤后的基础上做分组统计按部门分组计算每组人数和平均薪资并且用 HAVING 过滤掉人数小于 2 的部门SELECT department, COUNT(*) AS emp_count, AVG(salary) AS avg_salary FROM employee WHERE hire_date 2020-01-01 AND salary 13000 AND gender M GROUP BY department HAVING COUNT(*) 2;注意这里的执行顺序WHERE 先过滤原始数据然后 GROUP BY 分组COUNT 数每组人数HAVING 再把小组过滤掉。这个顺序在脑子里再过一遍确保能讲清楚为什么 HAVING 可以过滤 COUNT(*) 的结果而 WHERE 不行。第三步加排序和分页按平均薪资降序取前 3SELECT department, COUNT(*) AS emp_count, ROUND(AVG(salary), 2) AS avg_salary FROM employee WHERE hire_date 2020-01-01 AND salary 13000 AND gender M GROUP BY department HAVING COUNT(*) 2 ORDER BY avg_salary DESC LIMIT 3;这里我用 ROUND 函数把平均薪资保留两位小数这是报表场景里很常见的小处理原始平均值可能是一长串小数不处理没法看。执行完成后你会得到符合需求的结果集。整个过程中每一步都建议先单独跑一遍确认上一步的结果正确再往下一步走。我见过的很多 SQL 问题都是因为“想一步到位”结果中间逻辑错了排查的时候反而更费劲。6.3 执行顺序全回顾这个案例把单表查询的核心语法全用了一遍把执行顺序再完整理一遍这条链子务必记住FROM employee确定数据源WHERE 条件过滤先对原始表的每一行做判断不符合的直接丢弃GROUP BY department把过滤后的行分组聚合函数计算对每个组分别计算 COUNT、AVG 等HAVING COUNT(*) 2对组结果过滤SELECT 投影确定最终输出的列计算别名ORDER BY avg_salary DESC对最终结果排序LIMIT 3截取前 3 行很多看似诡异的 SQL 行为比如“为什么 WHERE 里不能用别名”“为什么 HAVING 可以引用聚合函数”只要你把这个顺序刻在脑子里全都解释得通。面试问执行顺序直接背这一段就够了比死记硬背一百道面试题都管用。7. 常见问题排查与避坑指南7.1 连接不上 MySQL 的排查思路这是所有 MySQL 新手最常卡住的第一道坎。明明安装好了执行mysql -uroot -p却报错最常见的一种是ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock (2)这个报错在 Linux 和 macOS 上很常见本质上是客户端在本地找不到 MySQL 服务的 socket 文件。出现这个错误时先别急着重装按顺序排查第一步确认服务是否启动。Linux 下执行systemctl status mysqld或service mysql statusmacOS 下用brew services list。如果服务没起先启动服务再来连接。第二步确认 socket 文件路径。有时候 MySQL 的 socket 文件不默认在 /tmp 下而是在 /var/run/mysqld/ 或自定义路径。可以在 MySQL 配置文件 my.cnf 的 [mysqld] 段查看socket配置连接时手动指定mysql -uroot -p -S /var/run/mysqld/mysqld.sock。第三步确认 TCP 方式能否连接mysql -uroot -p -h127.0.0.1 -P3306。用 TCP 能连而 socket 不能连大概率就是 socket 路径配置的问题。这套排查顺序适用于绝大多数连接类报错包括 MySQL Workbench 或 Navicat 连接时报 2003 错误那是端口不通或服务没起。Windows 上常见的是缺少 MySQL 服务或端口被占用。先打开服务管理器确认 MySQL 服务存在且已启动再执行netstat -ano | findstr 3306看看端口是否被占用如果被占用把对应的进程处理掉或者给 MySQL 换一个端口。装完 MySQL 连不上的问题90% 以上出在这几个点上把这些检查完基本就能排除。7.2 SQL 执行报错的几个典型场景单表查询本身语法不复杂但新手报错集中在几个地方我把高频报错整理成一张速查表遇到类似问题直接对照排查报错现象常见原因解决办法Unknown column xxx in field list列名写错或列名不存在用SHOW COLUMNS FROM employee;查实际列名语法错误 near xxx关键字拼错、括号不匹配、字符串少引号仔细检查语句注意字符串值要加单引号使用了 GROUP BY 的列不合法错误SELECT 里的列不在 GROUP BY 里且没被聚合把裸列去掉或加进 GROUP BY排序字段不在 SELECT 中报错ORDER BY 引用了一个不存在的列名检查列名拼写中文乱码表或连接字符集不是 utf8mb4建表时指定 DEFAULT CHARSETutf8mb4连接时执行SET NAMES utf8mb4;我特别想强调一个容易踩的坑字符串里的单引号。SQL 里字符串值用单引号包裹如果字符串本身包含单引号比如查WHERE name OBrien要用两个单引号转义这跟编程语言里的转义思路不一样。很多新手在这里折腾半天其实只是转义没搞对。年龄和薪水的数字比较不需要加引号日期字符串建议显式加引号虽然 MySQL 在比较时会对类型做隐式转换但依赖隐式转换容易留下隐患不如老老实实写规范类型。比如WHERE salary 15000能跑通但会让索引失效的概率增加尤其是字段类型和比较值类型不一致时MySQL 可能放弃索引走全表扫描。这条经验在生产环境踩过坑所以在这里多说一句。7.3 查询结果与预期不符的排查方法还有一种很常见的状况SQL 不报错但查出来的结果和预期不符。这种问题最让人头大因为没报错信息可看。我的排查习惯是“逐步缩小问题范围”具体做法如下。先看最小结果集先执行SELECT * FROM employee WHERE ...加上你能想到的最宽松条件确认这批数据在不在表里。比如你想查技术部的人发现结果少了人先不带任何条件地SELECT * FROM employee看看那张表的全部数据人到底在不在。有时候问题根本不是 SQL 写错了而是数据压根没插进去或者插到了别的表。再看条件是否符合业务逻辑。比如WHERE salary 15000和WHERE salary 15000的边界值正好差了一个人的问题检查一下“大于”和“大于等于”的口径是不是你想要的。NULL 值也经常在这里暗算你一个 salary 为 NULL 的行在WHERE salary 0里不会被选出因为 NULL 参与比较的结果是“未知”不算真。很多人写过滤条件时没考虑 NULL导致结果比预期少了几行回头检查数据才发现那几行是 NULL。最后是用 HAVING 还是 WHERE 这个老问题再强调一次WHERE 在分组前过滤HAVING 在分组后过滤。如果你把本应在分组后做的过滤条件写在 WHERE 里结果很可能空或者不对。比如上面实战案例里“人数不少于 2 的部门”这个条件必须用 HAVING因为人数得先分组数出来才能判断写成WHERE COUNT(*) 2直接报错因为 WHERE 执行时还没有分组COUNT(*) 根本没法用。7.4 顺手积累的几个实用技巧文章最后分享几个平时写单表查询时积累的顺手技巧对后续学习也很有帮助。第一SQL 的习惯性缩进。字段列表一行一个运算符前后有空格WHERE 和 AND 对齐。这不是为了好看而是当 SQL 越来越长时好的排版能让你一眼看到语句结构排错效率提高很多。尤其是 GROUP BY、ORDER BY、WHERE 三个关键字竖着对齐以后执行顺序一目了然。养成这个习惯后写复杂 SQL 时受益很大。第二用 EXPLAIN 看执行计划。在任何查询前面加EXPLAINMySQL 会告诉你这条语句打算怎么执行——用什么方式访问表、可能扫描多少行、有没有用到索引。比如EXPLAIN SELECT name, salary FROM employee WHERE salary 15000;看输出里的 type 列和 key 列如果 type 是 ALL 且 key 是 NULL说明在走全表扫描没用到索引。这不是单表查询必须掌握的内容但早一点养成看 EXPLAIN 的习惯后面写复杂查询、调优时能少走很多弯路。第三刻意用“自问自答”的方式练 SQL。学查询最忌讳看会了不手写。我的建议是把文章里的每个例子先自己抄一遍再改条件跑一遍比如把部门换成“产品部”把薪水门槛改成 20000把排序改成升序。别小看这几个微小的改动它能帮你确认自己是真的理解语法而不是在照抄。练到能不看示例写出一个带 WHERE GROUP BY HAVING ORDER BY LIMIT 的完整查询时单表查询这门课就算真正过关了。之后再进入多表 JOIN、子查询会发现有了这个底子学起来顺畅很多。