资讯详情

资讯详情

MySQL单表查询实战:从基础语法到综合练习

MySQL 单表查询其实是整个 SQL 学习路线里性价比最高的一块。从大学课程、培训机构、到面试题单表查询都是最先考、最常考、也最容易出细节坑的部分。很多同学觉得单表查询不就是 SELECT FROM WHERE等真正面对一道带条件、排序、分组、分页、NULL 判断的综合题时反而写不流畅。这篇是 MySQL 系列的第 9 篇专门用来做单表查询的集中练习。本文会把练习环境、测试数据、查询语句、结果含义、常见报错全部走一遍。你可以直接复制 SQL 到自己的 MySQL 里跑也可以先把练习题抄下来自己写一遍再对答案。适合刚学完 SQL 基础、想巩固语法的初学者也适合准备面试、想快速捡起 MySQL 查询能力的开发者。先给一个全文速览能力项说明适用数据库MySQL 5.7 / MySQL 8.0 均可前置条件能连接到 MySQL 服务有建库建表权限核心语法SELECT / WHERE / LIKE / IN / BETWEEN / ORDER BY / LIMIT进阶语法GROUP BY / HAVING / 聚合函数 / DISTINCT练习数据自建员工表 emp约 14 条记录工具选择命令行、Navicat、MySQL Workbench 都可以涉及接口本练习不涉及 API重点是把 SQL 写对、跑通、看懂结果如果你 MySQL 还没装好建议先按对应系统的安装教程把环境搭好确保能通过命令行或图形工具连上服务再开始下面的操作。1. 单表查询练习前的环境准备开始之前先确认你的 MySQL 服务已经正常启动。命令行登录mysql -u root -p输入密码后能进入mysql提示符就算成功。如果你用的是 Navicat 或 MySQL Workbench直接新建连接测试连通性即可。接下来我们要把练习用的数据库和表准备好。整个过程都在你本机或你自己的测试库中进行避免动到生产环境的数据。我建议单独建一个库方便后面练习完直接删除。CREATE DATABASE IF NOT EXISTS company DEFAULT CHARACTER SET utf8mb4; USE company;这里指定utf8mb4字符集主要原因是后续练习中会涉及中文排序、中文模糊匹配字符集不对会踩很多莫名其妙的坑。然后建员工表DROP TABLE IF EXISTS emp; CREATE TABLE emp ( emp_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 员工编号, emp_name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男 COMMENT 性别, dept VARCHAR(30) COMMENT 部门, job VARCHAR(30) COMMENT 岗位, salary DECIMAL(10,2) COMMENT 月薪, bonus DECIMAL(10,2) COMMENT 奖金, city VARCHAR(30) COMMENT 城市, hire_date DATE COMMENT 入职日期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工表;表结构不要太复杂保留练习单表查询需要的核心字段即可。这里有数值类型、字符串类型、日期类型还有允许为空的bonus字段方便后续练习NULL判断。插入测试数据INSERT INTO emp (emp_name, gender, dept, job, salary, bonus, city, hire_date) VALUES (张三, 男, 技术部, Java开发, 15000.00, 3000.00, 北京, 2020-03-15), (李四, 女, 技术部, 前端开发, 13000.00, 2500.00, 上海, 2021-07-01), (王五, 男, 产品部, 产品经理, 18000.00, 5000.00, 北京, 2018-11-20), (赵六, 女, 设计部, UI设计师, 11000.00, 1500.00, 深圳, 2022-01-10), (钱七, 男, 技术部, 测试工程师, 10000.00, 1000.00, 广州, 2022-06-15), (孙八, 女, 运营部, 运营专员, 9500.00, 800.00, 北京, 2023-02-28), (周九, 男, 技术部, 运维工程师, 14000.00, 2000.00, 上海, 2019-09-01), (吴十, 女, 人事部, HRBP, 12000.00, NULL, 北京, 2021-12-05), (郑一, 男, 财务部, 会计, 10500.00, 1200.00, 杭州, 2020-08-17), (冯二, 女, 销售部, 销售经理, 20000.00, 8000.00, 广州, 2017-04-12), (陈三, 男, 销售部, 销售顾问, 9000.00, 3500.00, 深圳, 2022-10-08), (褚四, 女, 技术部, 数据分析师, 16000.00, 2800.00, 上海, 2019-05-23), (卫五, 男, 产品部, 运营助理, 8000.00, 500.00, 北京, 2023-07-10), (蒋六, 女, 设计部, 交互设计师, 12500.00, 1800.00, 杭州, 2021-03-30);插完后先看一遍全量数据确认没有任何问题SELECT * FROM emp;这里有一个很容易犯的错误很多人插入数据后发现中文变成乱码或者插入直接报错。绝大多数原因是客户端连接字符集和表字符集不一致。可以先设置本次连接的字符集再执行插入SET NAMES utf8mb4;后面所有练习都基于这张 14 条记录的员工表。如果你想自己加数据也可以但要注意bonus字段保留几条NULL后面专门针对它出题。2. 单表查询基础SELECT 与 WHERE2.1 查询指定列先从最简单的开始。查询所有员工的姓名、岗位、月薪SELECT emp_name, job, salary FROM emp;结果是三列行数与全表一致。这里要理解一个关键点SELECT 决定的是查询哪些列WHERE 决定的是过滤哪些行两者职责不同不要混在一起想。查看所有不重复的部门列表SELECT DISTINCT dept FROM emp;DISTINCT会去掉重复值。从这个结果能看到当前表里总共有几个部门后面分组统计时还会用到。注意DISTINCT是对后面所有列的组合去重不是只对第一个列去重。如果写SELECT DISTINCT dept, city FROM emp它要求部门加城市的组合不重复。2.2 WHERE 条件过滤找出月薪大于等于 15000 的员工SELECT emp_name, job, salary FROM emp WHERE salary 15000;从数据看符合条件的应该是王五、冯二、褚四这几个人。条件判断支持常规的比较运算符、、、、、!或。比较运算符两边可以是列名也可以是常量甚至是另一个字段。比如查奖金比月薪高的员工这里本身就是单表内两个字段之间的比较SELECT emp_name, salary, bonus FROM emp WHERE bonus salary;这类字段与字段比较的题目非常容易出现在笔试题里因为很多人的思路还停留在字段与常量比较想不到直接写bonus salary。多个条件组合时用AND、OR连接SELECT emp_name, dept, salary FROM emp WHERE dept 技术部 AND salary 12000;这里要注意运算符优先级的坑。AND的优先级高于OR所以如果一个 SQL 里同时出现AND和OR又想要不同的逻辑组合就一定要加括号。比如SELECT emp_name, dept, salary FROM emp WHERE (dept 技术部 OR dept 产品部) AND salary 12000;不加括号的话SQL 会先执行dept 技术部 OR dept 产品部 AND salary 12000由于AND优先级高结果等价于dept 技术部 OR (dept 产品部 AND salary 12000)查询语义完全不一样。这是一种非常常见又隐蔽的查询错误自己写 SQL 时一定要养成加括号的习惯。2.3 基础练习先自己写再往下看答案。题目一查询销售部所有员工的姓名、岗位、城市。题目二查询月薪在 12000 到 16000 之间的员工姓名和工资。参考答案SELECT emp_name, job, city FROM emp WHERE dept 销售部;SELECT emp_name, salary FROM emp WHERE salary 12000 AND salary 16000;第二题后面还会讲到BETWEEN AND写法但这道题先让你体会一下AND连接两个条件的感觉。3. 模糊查询与范围查询3.1 LIKE 模糊匹配模糊查询在单表查询里非常重要因为真实业务场景中你经常只知道某个名字的一部分。比如查所有姓张的员工SELECT emp_name, job FROM emp WHERE emp_name LIKE 张%;%代表任意多个字符包括零个字符。所以张%能匹配到张三。如果写成%张%它匹配的是姓名中间任何位置带张的人这种写法在搜索功能里更常用。下划线_只匹配一个任意字符。比如查名字里第二字是三的人SELECT emp_name FROM emp WHERE emp_name LIKE _三%;这个_匹配张这个位置三是第二个字%匹配后面的内容。%和_的区别必须分清楚面试的时候经常被问到。3.2 BETWEEN AND 与 IN查月薪在 12000 到 16000 之间的员工可以用BETWEEN ANDSELECT emp_name, salary FROM emp WHERE salary BETWEEN 12000 AND 16000;注意BETWEEN AND是包含边界值的也就是包含 12000 和 16000 本身。如果你不希望包含边界还是要用和手动组合。这一点在按时间范围查询时特别容易被忽略很多人在统计月初月末数据时因为边界条件没写对导致数据多一条或少一条。查城市为北京、上海、广州的员工SELECT emp_name, city FROM emp WHERE city IN (北京, 上海, 广州);IN后面是一个列表等价于city 北京 OR city 上海 OR city 广州。它是多个 OR 条件的语法糖但可读性更好。反过来想排除这几个城市的员工写成SELECT emp_name, city FROM emp WHERE city NOT IN (北京, 上海, 广州);3.3 NULL 的判断这是最容易出错的一个点。查询奖金为空的员工SELECT emp_name, bonus FROM emp WHERE bonus IS NULL;注意bonus NULL这种写法在 MySQL 里不会报错但永远查不出数据。NULL不是一个值它表示未知用去比较未知结果仍然是未知所以在条件判断中不会为真。判断空值必须用IS NULL判断非空用IS NOT NULL。你还会遇到另一个问题用IFNULL函数可以在查询结果里把NULL转成指定值。比如SELECT emp_name, IFNULL(bonus, 0) AS actual_bonus FROM emp;这里的AS是给查询结果列取别名。注意IFNULL只是在查询展示时把NULL转换成 0并没有真正修改原始数据。这个点在后端查询里非常实用因为很多语言在处理NULL时会类型报错。3.4 练习与排查题目三查询岗位上带有开发两个字的员工。题目四查询奖金不是空的员工。参考答案SELECT emp_name, job FROM emp WHERE job LIKE %开发%;SELECT emp_name, bonus FROM emp WHERE bonus IS NOT NULL;如果你查LIKE写对了却查不到中文先检查字符集再检查数据里是否包含空格或全半角不一致。中文数据里最隐蔽的问题是搜索条件里出现全角空格肉眼根本看不出来。建议用SELECT LENGTH(emp_name) FROM emp检查字段长度是否异常。4. 排序与分页4.1 ORDER BY 基本用法单表查询如果没有排序结果顺序是不保证的尤其是 InnoDB 存储引擎下记录在物理文件中的顺序可能和数据插入顺序不一致。所以想让结果具备可预测性必须显式加ORDER BY。按月薪从高到低排序SELECT emp_name, salary FROM emp ORDER BY salary DESC;DESC表示降序ASC表示升序升序是默认值所以只写ORDER BY salary就是升序。多字段排序时先写优先排序的字段。比如先按部门升序再按薪资降序SELECT dept, emp_name, salary FROM emp ORDER BY dept ASC, salary DESC;这个查询结果的逻辑是先按部门字母或字符编码顺序排好同一部门内部再按工资从高到低排。这里想强调一个实际开发中常见的误区多字段排序时如果前一个字段已经能区分顺序后一个字段就不会生效只有前一个字段值相同时后一个字段才起作用。你在刚学的时候容易想成每个字段独立排序再合并实际不是这样。4.2 中文排序问题如果表字符集是utf8mb4直接用ORDER BY emp_name ASC排序中文时一般情况下 MySQL 是按 Unicode 编码排序的并不按照拼音或偏旁部首来排结果看起来经常不符合直觉。想在 MySQL 5.7 里按拼音排序可以借助CONVERTSELECT emp_name FROM emp ORDER BY CONVERT(emp_name USING gbk) ASC;MySQL 8.0 里由于默认字符集和排序规则已经有改进普通场景下直接排序基本可用但如果你遇到中文排序结果不符合预期的场景仍然可以用CONVERT(emp_name USING gbk)这种处理方式。这里不展开深入只要记住中文排序异常时先改排序规则这个方向就可以了。4.3 LIMIT 分页分页是后端联表查询之前必须掌握的技能。每页显示 5 条数据查询第一页SELECT emp_id, emp_name, salary FROM emp ORDER BY salary DESC LIMIT 0, 5;LIMIT 0, 5的意思是跳过 0 条取 5 条。第二页SELECT emp_id, emp_name, salary FROM emp ORDER BY salary DESC LIMIT 5, 5;偏移量计算公式是(页码 - 1) * 每页条数。这个公式几乎是后端开发写分页时必用的但要注意如果OFFSET非常大比如几百万行后再取数据MySQL 仍然要扫描并丢弃前面大量行性能会很差这就是深分页问题。这个问题在单表查询阶段只需了解真正处理时可以用WHERE id 上一页最大id的方式替代。4.4 排序与分页练习题目五查询工资排名第 6 到第 10 的员工姓名和工资。参考答案SELECT emp_name, salary FROM emp ORDER BY salary DESC LIMIT 5, 5;排序和分页经常一起出现因为分页本身基于排序结果才有意义。如果你没有写ORDER BY就直接LIMIT拿到的数据顺序是不确定的每次运行可能一样也可能不一样。排查线上分页数据变来变去的问题时第一件事就是检查排序字段是否完整。5. 聚合函数与分组统计5.1 常用聚合函数聚合函数是对一组值执行计算并返回一个单一值的函数。最常用的几个是SELECT COUNT(*) AS total_count, SUM(salary) AS total_salary, AVG(salary) AS avg_salary, MAX(salary) AS max_salary, MIN(salary) AS min_salary FROM emp;COUNT(*)统计的是行数COUNT(bonus)统计的是 bonus 字段非 NULL 的行数。这一点要格外注意如果你是统计奖金不为空的人数应该用COUNT(bonus)但如果你只是想统计表里总共几条记录用COUNT(*)。两者在bonus字段有 NULL 时结果完全不同。SUM、AVG会自动忽略 NULL 值。AVG(bonus)等于SUM(bonus)/COUNT(bonus)这个细节也会影响统计结果的含义。5.2 GROUP BY 分组统计分组的核心作用是把数据按某个维度拆分再对每个分组做聚合计算。统计每个部门的员工人数SELECT dept, COUNT(*) AS cnt FROM emp GROUP BY dept;统计每个城市的平均工资SELECT city, AVG(salary) AS avg_salary FROM emp GROUP BY city;这里有一个所有 MySQL 初学者都会遇到的坑如果查询列表里出现了非聚合列但它没有包含在GROUP BY子句中在 MySQL 5.7 且开启ONLY_FULL_GROUP_BY模式时会直接报错。例如下面的写法在很多数据库里都不能执行SELECT dept, emp_name, COUNT(*) FROM emp GROUP BY dept;因为emp_name既不在GROUP BY中也不是聚合函数的结果。当部门里有多个员工时到底展示哪个emp_name是没有语义的。开发时遇到this is incompatible with sql_modeonly_full_group_by这类报错就要检查是不是 SELECT 中混入了非聚合字段。更稳妥的做法是查询列表里只保留分组字段和聚合函数其他字段如果想要可以通过子查询或另一次查询实现。5.3 HAVING 过滤分组WHERE是在分组前过滤原始行HAVING是在分组后过滤分组。统计平均工资超过 12000 的部门SELECT dept, AVG(salary) AS avg_salary FROM emp GROUP BY dept HAVING AVG(salary) 12000;这里可以直观看到 SQL 的执行顺序大概是FROM找表WHERE过滤行GROUP BY分组HAVING过滤分组SELECT计算输出列ORDER BY排序LIMIT分页。理解这个顺序是写复杂查询的基础也是排查各种为什么结果不对问题的钥匙。HAVING后面能不能用SELECT中的别名MySQL 中HAVING是可以识别别名的但为了可读性更推荐直接写原始表达式因为不同的数据库对HAVING别名支持情况并不一致。5.4 DISTINCT 与 GROUP BY去重有两种方式SELECT DISTINCT dept FROM emp和SELECT dept FROM emp GROUP BY dept。这两者结果相似但思路完全不同。DISTINCT是去重输出GROUP BY是分组统计。如果只是想知道有哪几个部门用DISTINCT更直观如果还要统计每个部门的人数或工资那就必须用GROUP BY。统计每个部门的最高工资并降序排列SELECT dept, MAX(salary) AS max_salary FROM emp GROUP BY dept ORDER BY max_salary DESC;这里SELECT中定义了别名max_salaryORDER BY中使用别名是可以的因为ORDER BY在SELECT之后执行。但WHERE中不能使用别名因为WHERE在SELECT之前执行。如果你在WHERE里写WHERE max_salary 10000MySQL 会报Unknown column错误。这个执行顺序是面试常考的点也是写复杂 SQL 时最容易疑惑的地方。5.5 分组统计练习题目六统计每个部门的人数按人数降序排列。题目七统计部门人数大于等于 2 的部门。参考答案SELECT dept, COUNT(*) AS cnt FROM emp GROUP BY dept ORDER BY cnt DESC;SELECT dept, COUNT(*) AS cnt FROM emp GROUP BY dept HAVING COUNT(*) 2;第二题就是先GROUP BY dept统计人数再用HAVING过滤。注意这里不能把COUNT(*) 2写到WHERE里因为WHERE执行的时候分组还没发生此时COUNT(*)根本还没计算出来。6. 单表查询常见错误与排查问题现象可能原因排查方式解决方案查询不出 NULL 记录用了 NULL检查条件是否写成IS NULL改为WHERE bonus IS NULL中文模糊查询查不到字符集不一致或存在全角空格用LENGTH()检查字段长度设置SET NAMES utf8mb4清洗空格中文排序不符合预期utf8mb4默认排序规则不支持拼音排序查看当前排序规则使用CONVERT(字段 USING gbk)排序分组统计报only_full_group_by错误SELECT 中有非聚合列未包含在 GROUP BY检查报错的确切列只保留分组字段和聚合函数WHERE 中使用别名报未知列执行顺序先 WHERE 后 SELECT确认是否在 WHERE 中用别名把别名条件移到 HAVING 或以原始列重写LIMIT 100000, 20查询越来越慢深分页导致大量扫描用EXPLAIN观察扫描行数改为基于主键和上一页最大 ID 的查询这些坑每一个我都见过不止一次出现在线上问题和面试题里。单独看每个都简单但实际写查询的时候各种细节叠加在一起问题就变得很难定位。排查 SQL 问题的时候最好的方式是把查询拆成一步一步先跑SELECT * FROM emp再逐步加WHERE加GROUP BY加HAVING加ORDER BY最后加LIMIT每一步观察结果是否符合预期很快就能定位到是哪一步出了问题。7. 单表查询的性能观察EXPLAIN在练习阶段就建立用EXPLAIN的习惯对后面优化帮助很大。EXPLAIN不会真正执行查询而是展示 MySQL 执行这条 SQL 的大致计划。最基本的用法是EXPLAIN SELECT * FROM emp WHERE dept 技术部;返回结果里重点看几个字段字段含义怎么看type访问类型一般为 ALL、range、ref、eq_ref、const 等从差到好大致顺序为 ALL、index、range、ref、constpossible_keys可能用到的索引如果为 NULL说明没有可用索引key实际使用的索引如果为 NULL说明没用索引rows预计扫描行数越小越好Extra额外信息出现Using filesort表示文件排序可能需要优化排序字段刚刚那条dept 技术部如果没有给dept建索引type一般是ALLrows是 14。给dept建一个普通索引后CREATE INDEX idx_emp_dept ON emp(dept);再执行EXPLAINtype通常会变成refrows会明显减少。这个变化过程比直接背优化理论直观得多。查询完成后为了保持练习环境干净可以删除测试索引DROP INDEX idx_emp_dept ON emp;需要强调的是EXPLAIN只是一个执行计划估算它不代表真实执行时间。有时候type是ALL但因为表很小实际也很快有时候type是ref但访问的行数非常多也不一定最优。所以练习阶段重点不是抠每个参数而是建立查询能不能走索引、大概扫描多少行这个意识。后面学多表联查和慢查询优化时这个底子很重要。8. 单表查询综合练习前面按知识点拆开练习下面给一套综合题目建议不要看任何参考按顺序独立写完再对照参考答案。题目八查询技术部工资大于 12000 的员工姓名和工资按工资降序排列。题目九统计各部门工资总和输出高于 25000 的部门。题目十查询 2020 年之后入职的员工信息。参考答案SELECT emp_name, salary FROM emp WHERE dept 技术部 AND salary 12000 ORDER BY salary DESC;SELECT dept, SUM(salary) AS total_salary FROM emp GROUP BY dept HAVING SUM(salary) 25000 ORDER BY total_salary DESC;SELECT * FROM emp WHERE hire_date 2020-01-01;第十题里日期比较在 MySQL 中可以直接用字符串形式MySQL 会自动把字符串转换为日期进行比较。所以记住一个通用的经验法则在条件判断里字段类型是日期就直接给标准格式字符串字段类型是数字尽量给数字字段类型是字符串就加引号不要自作聪明做隐式类型转换。如果你写WHERE salary 10000虽然在 MySQL 中多数情况下也能得到结果但当字段有索引时字符串和数字比较可能导致索引失效进而产生全表扫描这类隐式转换问题在慢查询优化中是一个非常经典的反面教材。9. 最佳实践与学习建议先建练习专用库不要直接在生产环境甚至业务库上反复执行DROP TABLE和大量INSERT操作。练习结束后想清空这个库可以直接执行DROP DATABASE company不会影响其他数据。所有练习脚本建议用 SQL 文件保存而不是只在客户端里敲完就没了。包括建表、插入、每个查询题目和对应的答案保存成一个带编号的文件比如01_create_table.sql、02_insert_data.sql、03_query_exercise.sql这样以后复习或换电脑都能快速恢复环境。命令行执行 SQL 文件的方式mysql -u root -p company ./query_exercise.sql写查询时养成格式化习惯关键字大写或统一小写都可以但一个项目里最好统一SELECT的每个字段放在单独一行多个查询条件用括号明确优先级。这样不仅方便自己排查代码评审时别人也更容易看懂。关于字符集建库时显式指定utf8mb4连接时执行SET NAMES utf8mb4这是避免中文乱码最稳妥的组合。另外文档和代码注释里不要写一堆必会、重点之类的套话把每条 SQL 解决什么问题写清楚就够了。如果后面要接入 Java、Python 等编程语言这套 SQL 语句可以原样放到 JDBC、PyMySQL、MyBatis 的 SQL 语句中。但要注意不要把这些练习数据直接用于真实业务系统真实系统的数据往往有主外键约束、逻辑删除字段、多租户隔离等额外要求单表查询练习阶段可以先不考虑这些。10. 总结与下一步单表查询练到位了真正跨进多表查询时会顺畅很多。因为你已经知道了 WHERE 执行顺序、GROUP BY 和 HAVING 的边界、NULL 判断、聚合函数取舍、索引和 EXPLAIN 的基本概念这些是所有复杂查询的公共底座。建议你先完整跑一遍本文的建表和插入 SQL然后不参考答案把十道练习题目都写一遍。重点验证三件事第一NULL 的判断是否真的理解第二GROUP BYHAVING和WHERE的执行顺序是否清楚第三碰到only_full_group_by报错时能不能自己读懂报错含义并调整查询。最容易踩的坑还是这几处$ NULL$永远查不到数据、WHERE里用别名导致未知列、中文排序结果不符合预期、分组查询混入非聚合列。如果你在自己的练习中遇到这些问题回到对应章节检查即可。下一步可以直接进入多表查询练习也就是INNER JOIN、LEFT JOIN、子查询这几个方向。学的时候记得带着单表查询的执行顺序思维你会发现多表查询的很多奇怪现象本质上还是单表查询规则的延伸。这篇文章可以按 F12 收藏或者复制到自己的笔记里遇到查询语法问题时回来对照执行顺序和排查表比盲猜快得多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →