查数据这件事,说难不难,说简单也不简单。我见过不少刚接触数据库的朋友,建表、插数据都挺利索,一到写查询语句就卡壳,要么忘了加条件把全表捞出来,要么条件写错查出来一堆不对的东西。这篇是零基础系列的第八讲,前面咱们已经聊过安装、建库、建表、插入数据这些基础操作,今天就把最常用的查询语句一次讲透,学完你就能自己写SELECT查想要的数据了。
1. 项目概述:查询在数据库里的地位
1.1 这讲要解决什么问题
数据库的本质就是存数据和取数据,存了半天取不出来,或者取出来的不是自己想要的,前面所有操作都白搭。查询(SELECT)就是“取数据”的手段,也是日常开发里出现频率最高的一类SQL语句。CRUD四个操作里,增删改加起来都没查用得多。
这篇文章的核心目标是:让完全没写过SQL的读者,掌握基础查询的完整套路。我会从最简单的“把整张表捞出来看一眼”开始,一步步讲到条件筛选、排序、去重、限量取数,最后用一个完整的实操案例把知识点串起来。学完之后,面对“查一下成绩在80分以上的学生”“按时间倒序看最近10条记录”这类需求,你能顺手写出对应的SQL,而不是到处复制粘贴再瞎改。
1.2 SELECT的查询思路
SELECT语句的书写顺序和执行顺序是两个不同的东西,这个点很多人学了很久才反应过来。书写顺序是 SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT,一眼看上去很规整。但真正执行的时候,数据库是先定位到 FROM 后面的那张表,然后根据 WHERE 条件过滤掉不要的行,再做后续的排序、分组等操作,最后才决定 SELECT 后面要展示哪些列。
理解这个顺序非常重要。举个例子,你想查“名字叫张三的学生”,SQL写成了 SELECT 姓名 FROM 学生表 WHERE 姓名 = '张三',看起来没问题。但如果你以为数据库是先按 SELECT 把“姓名”这一列取出来再过滤,后面学 JOIN、子查询的时候就会越绕越晕。先有数据集合,后有列展示,这个观念建议现在就建立起来。
提示:零基础学查询,先别急着背全部语法。把“查哪张表、过滤什么条件、展示哪些列”这三个问题想清楚,SQL自然就写出来了。
2. 核心语法拆解:从最简单的SELECT开始
2.1 查整表和指定列
最简单的一条查询,就是不带任何条件把整张表的数据全部捞出来。语法长这样:
SELECT * FROM 表名;星号(*)代表“所有列”。这条语句在数据量小的时候用起来特别爽,刚建完表想看看到底存了什么,一行命令全出来了。但我要提醒一句:生产环境里尽量不要用 SELECT *,原因有两个。第一,如果表有几十个字段,而你只需要其中两三个,把全部字段查出来会白白消耗网络带宽和数据库资源;第二,代码里用 SELECT * 一旦表结构变更,程序里拿到的字段可能对不上,排查起来非常痛苦。自己本地学习随便查,线上代码一定老老实实写列名。
指定列查询的写法就是 SELECT 后面列出你要的列名,用逗号分隔:
SELECT id, name, score FROM student;这条语句和上面的区别在于,数据库只返回 id、name、score 这三列,其他字段一概不管。列的顺序是按你 SELECT 后面写的顺序来的,不是按表结构里的顺序。这一点可以用来调整展示逻辑,比如你想把成绩列放在名字前面,直接交换写就行了。
还有一个经常被忽略的小技巧:查询时给列起别名。别名的英文关键字是 AS,中文场景下用得很频繁:
SELECT id AS 学号, name AS 姓名, score AS 成绩 FROM student;给列起别名之后,查询结果的头一行就会显示中文别名,比直接看英文列名直观得多。这个操作在实际开发里也很常见,尤其是数据要给前端展示,或者要导出报表的时候,列名直接变成人能看懂的标题,省去后处理。
2.2 条件过滤WHERE怎么用
光会查全表远远不够,真实需求永远是“给我看看某一部分数据”。这部分数据怎么界定?靠 WHERE 子句。
WHERE 后面跟的是过滤条件,只有满足条件的行才会出现在结果里。它的核心逻辑和“筛子”一模一样:数据库把表里的每一行数据拿过来,按条件判断,放行或拦截。
最基础的过滤是等值比较。比如查学号等于1001的学生:
SELECT * FROM student WHERE id = 1001;这里要重点提一下等号的使用。在SQL里,判断两个值相等用的是单个等号(=),不是编程语言里的双等号(==)。这是新手最容易踩的坑之一,心里想着比较,手上一不小心写成两个等号,直接报语法错误。
除了等于,还有一整套比较运算符,用一个小表格列清楚:
| 运算符 | 含义 | 示例 | 说明 |
|---|---|---|---|
| = | 等于 | WHERE age = 20 | 完全匹配某个值 |
| != 或 <> | 不等于 | WHERE age != 20 | 排除某个值,两种写法都对 |
| > | 大于 | WHERE score > 60 | 注意不含等于 |
| < | 小于 | WHERE score < 60 | 注意不含等于 |
| >= | 大于等于 | WHERE score >= 60 | 含边界值 |
| <= | 小于等于 | WHERE score <= 60 | 含边界值 |
这六个运算符是最基础的,覆盖了绝大多数过滤需求。比如“查不及格的学生”就是 WHERE score < 60,“查非北京籍学生”就是 WHERE city != '北京'。
当你有多个条件要同时满足的时候,就需要用到逻辑运算符。最常见的是三个:AND(且)、OR(或)、NOT(非)。AND 表示前后两个条件都得成立,OR 表示满足其中一个就行,NOT 表示取反。组合起来可以写非常精确的过滤条件:
-- 查成绩大于60且小于90的学生 SELECT * FROM student WHERE score > 60 AND score < 90; -- 查班级为1班或者是女生 SELECT * FROM student WHERE class_id = 1 OR gender = '女'; -- 查不在北京市的学生 SELECT * FROM student WHERE NOT city = '北京';用 AND 和 OR 组合多个条件时,有个非常经典的坑:优先级问题。AND 的优先级高于 OR,也就是说数据库会先处理 AND 两边的条件,再处理 OR。如果你想把 OR 的两部分作为一个整体,必须加括号:
-- 正确写法:查1班或2班的所有女生 SELECT * FROM student WHERE gender = '女' AND (class_id = 1 OR class_id = 2); -- 错误写法:查出来的是全体女生,加上1班的男生,完全不对 SELECT * FROM student WHERE gender = '女' AND class_id = 1 OR class_id = 2;这种错误特别隐蔽,因为语句能正常执行、不报错,但结果就是不对。我的建议是:多条件组合的时候,不管有没有必要性,给每个逻辑组合都加上括号,既安全又清晰,别人读代码也省力。
2.3 模糊匹配与区间查询的实用技巧
精确匹配虽然常用,但现实需求里“模糊查一下”的情况更常见。比如你想找姓“张”的同学,不可能把所有张姓的名字都列出来再用等号匹配,这时候就要用 LIKE 模糊匹配。
LIKE 配合两个通配符使用,一个是百分号(%),代表任意长度的任意字符;一个是下划线(_),代表单个任意字符。我举个例子你就明白区别了:
-- 查所有姓张的学生:张后面跟什么都可以 SELECT * FROM student WHERE name LIKE '张%'; -- 查姓张且名字总共两个字的:张后面只能跟一个字 SELECT * FROM student WHERE name LIKE '张_'; -- 查名字里带“伟”的学生,不管伟在第几个位置 SELECT * FROM student WHERE name LIKE '%伟%';百分号放的位置不同,效果完全不同。'张%' 是“开头必须是张”,'%伟%' 是“中间或者开头结尾有伟都行”,'%龙' 是“结尾必须是龙”。这是模糊匹配里最值得动手实践的三个模式,建议自己建个小表反复试几遍,把位置和结果的关系彻底搞清楚。
除了模糊匹配,区间查询也经常碰到。“查价格在100到200之间的商品”“查日期在某个月内的订单”——这类需求用比较运算符加 AND 能写,但有更简洁的写法:BETWEEN AND。
-- 等价写法一:WHERE score >= 60 AND score <= 90 -- 等价写法二:BETWEEN 是包含边界值的 SELECT * FROM student WHERE score BETWEEN 60 AND 90;注意 BETWEEN 是包含两端边界值的,上面的写法等价于 >= 60 AND <= 90。如果你想排除边界,比如只要60到90之间不含60和90,就不能用 BETWEEN,得老老实实写成 score > 60 AND score < 90。
还有一个常用的 IN 关键字。当你需要匹配一组值中的任意一个,比如“查北京、上海、广州三个城市的学生”,用 OR 写是三个条件串起来,又长又容易漏括号。IN 写法则非常清爽:
SELECT * FROM student WHERE city IN ('北京', '上海', '广州');这个写法的效果等同于 city='北京' OR city='上海' OR city='广州',但可读性和扩展性好太多了。如果以后要加一个城市,直接在括号里加一个值就行,不用改整个条件结构。
3. 排序、去重与限量:让结果真正可读
3.1 ORDER BY排序的细节
查询出来的数据默认是按照存储顺序排列的,说人话就是“乱序”——你无法预测它按什么规则排。想要让结果按某个字段从大到小或者从小到大排列,就要用 ORDER BY。
排序的关键字是 ORDER BY,后面跟列名,再加上排序方向:ASC(ascending,升序,从小到大)或者 DESC(descending,降序,从大到小)。注意,ASC 可以省略,默认就是升序;DESC 必须明确写出来,不写的话数据库就给你按升序排。
-- 按成绩从小到大排序(升序) SELECT * FROM student ORDER BY score ASC; -- 按成绩从大到小排序(降序) SELECT * FROM student ORDER BY score DESC;排序还可以指定多个列,规则是“先按第一个列排,如果第一个列值相同,再按第二个列排”。比如先按班级排,同一个班级内再按成绩从高到低:
SELECT * FROM student ORDER BY class_id ASC, score DESC;这条语句的逻辑是:先把所有学生按班级从小到大排好,同一个班级里的学生按成绩从高到低排。多列排序在真实场景里用得非常频繁,“先按某个维度分组,组内再按另一个维度排序”这种需求到处都是。
有个容易忽略但实际很常见的需求:按中文排序。默认情况下,MySQL对中文字符串排序用的是字符编码的顺序,不是拼音顺序。如果你非要按拼音排,得额外指定排序规则(collation),这里不展开。对零基础来说,只需要知道“中文字段排序结果可能不是你想要的拼音顺序,需要另做处理”就足够了,真遇到这个问题再查资料。
3.2 DISTINCT去重
查询结果里经常会有重复值。比如你想知道“学生表里都来自哪些城市”,直接用 SELECT city FROM student 会把每个学生的城市都列一遍,出现几十个北京、几十个上海,完全没法看。这时候就要用 DISTINCT 去重。
SELECT DISTINCT city FROM student;这条语句会把城市字段里的重复值合并,北京只显示一次,上海只显示一次。注意,DISTINCT 修饰的是它后面所有列的组合,不是只修饰第一个列。比如:
SELECT DISTINCT class_id, city FROM student;这里去重的是“班级+城市”这个组合。只有当两条记录的 class_id 和 city 完全相同,才会被合并成一条。如果你只想对 class_id 去重,又想把 city 也显示出来,用 DISTINCT 是做不到的,这属于更高级的 GROUP BY 处理范畴,零基础阶段先清楚这个限定就行。
还有一个零基础容易踩的坑:DISTINCT 和 ORDER BY 一起用的时候,排序的字段必须出现在 SELECT 的列表里。比如:
-- 这样写可以,city在SELECT里 SELECT DISTINCT city FROM student ORDER BY city DESC; -- 这样写会报错或表现异常,因为age没在SELECT里 SELECT DISTINCT city FROM student ORDER BY age DESC;背后的原因是,去重之后 city 相同的记录已经被合并了,age 这个字段到底取哪一条完全不确定,数据库不允许这种语义模糊的操作。
3.3 LIMIT取前N条
数据量大起来之后,“想看看表里有什么”不能一页全捞出来,得只取前几条。LIMIT 就是干这个的。
LIMIT 有两种常见用法。第一种是数字参数,取前N条:
-- 取前10条记录 SELECT * FROM student LIMIT 10;第二种是双参数,实现分页效果:第一个参数是偏移量(从哪里开始),第二个参数是取多少条。偏移量从0开始计数,0代表第一条。
-- 从第6条开始取10条,也就是第6到第15条 SELECT * FROM student LIMIT 5, 10;这里有个零基础特别容易算错的点:LIMIT 5, 10 不是跳过5条再取10条吗?实际上它跳过了前5条记录,从第6条开始取,取10条。如果你要实现常见的分页公式,每页显示10条,查第3页的数据:
- 第1页:LIMIT 0, 10(跳过0条,取10条,即第1-10条)
- 第2页:LIMIT 10, 10(跳过10条,取10条,即第11-20条)
- 第3页:LIMIT 20, 10(跳过20条,取10条,即第21-30条)
规律就是:偏移量 = (页码 - 1) × 每页条数。这个公式建议抄在小本子上,后面做分页功能会反复用。
LIMIT 和 ORDER BY 的组合是最经典的高频搭配:“按时间倒序取最近10条记录”:
SELECT * FROM student ORDER BY create_time DESC LIMIT 10;这条语句的执行逻辑是:先把所有记录按时间从新到旧排好,然后取排在最前面的10条。非常简单直观,但执行效率在大表上会有问题,因为要先全表排序再取前N条。零基础阶段先不用关心性能,知道这个组合能实现“取最大的几个”这个需求就够了。
注意:LIMIT 的偏移量如果大于表里的记录总数,不会报错,只是返回空结果集。比如表里有5条数据,你写 LIMIT 10, 10,返回的就是空表。
4. 实操案例:从建表到查询一次跑通
4.1 准备测试数据
讲再多理论都不如亲手跑一遍。下面我带大家从头到尾建一张成绩表,插入几条测试数据,然后跑各种查询。先把环境准备一下:假设你已经能在MySQL命令行或者图形化工具里执行SQL语句了。
第一步,建一张学生的考试成绩表:
CREATE TABLE student_score ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20), class_id INT, gender VARCHAR(4), score INT, city VARCHAR(20), create_time DATETIME );建完表之后插入几条测试数据:
INSERT INTO student_score (name, class_id, gender, score, city, create_time) VALUES ('张伟', 1, '男', 85, '北京', '2024-01-05 08:30:00'), ('李娜', 1, '女', 92, '上海', '2024-01-05 09:00:00'), ('王强', 2, '男', 78, '北京', '2024-01-06 10:00:00'), ('刘敏', 2, '女', 65, '广州', '2024-01-07 11:30:00'), ('陈晨', 1, '男', 59, '上海', '2024-01-08 14:00:00'), ('杨静', 3, '女', 88, '深圳', '2024-01-09 16:20:00'), ('赵磊', 3, '男', 73, '北京', '2024-01-10 09:10:00'), ('孙丽', 2, '女', 96, '广州', '2024-01-11 10:45:00');插完之后先跑一条最基础的查询确认数据进去了没有:
SELECT * FROM student_score;这时候你看到的应该是8条完整的记录,每一条的所有字段都在。第一遍学到这里,强烈建议养成“插入后先查一遍”的习惯,后面所有查询操作都是建立在这个表之上的,确保数据正确是前提。
4.2 完成常用查询需求
数据准备好了,接下来就直接上手。假设产品经理一口气提了6个查询需求,我们逐个实现,把前面讲的知识点全部用上。
需求一:查所有成绩大于等于80分的学生,只需要姓名和成绩两列。
SELECT name, score FROM student_score WHERE score >= 80;需求二:查1班或者2班的女学生。这个需求用到了 AND 和括号的组合,注意括号不能少。
SELECT * FROM student_score WHERE gender = '女' AND (class_id = 1 OR class_id = 2);需求三:查所有姓“张”、“王”或“孙”的学生,用模糊匹配加 IN 组合处理。先看一下每个人名的姓氏分布:张伟、王强、孙丽共三个人符合要求。
SELECT * FROM student_score WHERE name LIKE '张%' OR name LIKE '王%' OR name LIKE '孙%';需求四:查成绩在60到90之间的学生,按成绩从高到低排序。
SELECT * FROM student_score WHERE score BETWEEN 60 AND 90 ORDER BY score DESC;这里执行结果应该包含张伟85、王强78、刘敏65、赵磊73四个人,不包含李娜92(超过90)也不包含陈晨59(低于60),因为 BETWEEN 是包含边界的。
需求五:统计所有学生来自哪些城市,去掉重复值。
SELECT DISTINCT city FROM student_score;需求六:查成绩最高的前三条记录。
SELECT * FROM student_score ORDER BY score DESC LIMIT 3;跑完这6个需求,你会发现每个查询本质上都是“选哪张表、用什么条件过滤、按什么顺序排、取多少条”。把需求翻译成这套逻辑,写SQL就没那么神秘了。
5. 常见问题与排查速查
5.1 查询结果不对怎么排查
查询写完了,结果不对,这几乎是每一位初学者每天都会遇到的事。结果不对通常分两种情况:报错了,或者没报错但数据不对。第二种更让人头疼,因为调起来没头绪。
先说“没报错但数据不对”。最常见的根因是条件写错了方向,比如大于写成了小于,等于写成了不等于。我的排查习惯是先看 WHERE 条到底过滤了什么,把条件单独拎出来验证。你可以把 SELECT 后面的列改成 *,看看返回的每一条记录是不是真的满足条件。比如查成绩大于等于80,出来的记录里有一条 score 是70,那多半是条件写错了,或者数据本身有更新但你没注意到。
第二个根因是 AND 和 OR 的优先级问题,这个在前面已经反复强调过。一旦发现查询结果比预期多或者比预期少,先检查条件里的括号有没有加对。比如预期是“1班或2班的女学生”,结果查出了大批1班的男生和2班的女学生,十有八九是缺了括号。
第三个根因是 NULL 值,这个是零基础最容易忽视的点。NULL 在SQL里表示“没有值”,它不是空字符串,也不是数字0。如果你用 score = NULL 去查询,一条结果都查不出来,因为 NULL 不能和任何值做等值比较。正确写法是 IS NULL 或 IS NOT NULL:
-- 正确:查没有填写成绩的学生 SELECT * FROM student_score WHERE score IS NULL; -- 错误:查不到任何结果,因为NULL无法用等号比较 SELECT * FROM student_score WHERE score = NULL;这一点多说一嘴:设计表的时候尽量给字段设定 DEFAULT 值,而不是放任 NULL。比如成绩字段默认给0,城市字段默认给空字符串,这样查询和理解都会省很多麻烦。
5.2 错误信息怎么看
MySQL报错会给你错误码和错误信息,但很多零基础学员一看到红色报错就慌,其实大部分错误都是小问题。常见的几种错误整理成表格,直接对照查:
| 错误现象 | 常见原因 | 解决方法 |
|---|---|---|
| ERROR 1064 (42000) SQL语法错误 | 语句拼写错误、关键字写错、少了逗号或括号 | 仔细检查整条语句,逐个词对照学习资料 |
| ERROR 1054 (42S22) Unknown column | 列名写错,表里根本没这个字段 | 用 DESC 表名 查看实际字段名 |
| ERROR 1146 (42S02) Table doesn't exist | 表名写错,尤其容易忽略大小写 | 确认表名拼写,注意数据库上下文 |
| 中文都显示为问号 | 客户端字符集不对 | 执行 SET NAMES utf8mb4; 再查询 |
排查语法错误有个笨但有效的方法:如果你用的是命令行,一条查询语句出错了,把语句拆开一段一段执行。先只写 SELECT * FROM 表名,通过了再加 WHERE,每加一段就验证一次,出错的点一下子就缩小了。
还有一个容易被忽略的问题:关键字冲突。如果你的列名或者表名恰好和MySQL的保留字相同,比如你有一个字段叫 order(订单),直接查 SELECT order FROM ... 会报错。解决办法是给这个字段加上反引号:
-- 反引号包裹关键字 SELECT `order` FROM 订单表;建表的时候尽量避开 order、group、select、where 这类保留字当字段名,不然后面每一次查询都得加反引号,非常烦人。
最后分享一个实战里特别有效的小习惯:在查询语句前加 EXPLAIN 关键字可以查看数据库是如何执行这条语句的,但零基础阶段用不上,知道有这么回事就行。现阶段只需要做到:一看出错信息,二查字段名,三检查括号和引号,90%的问题都能自己解决。SQL这个东西,写到后来拼的其实就是细心和熟练度。把一篇基础查询掌握扎实,后面学聚合函数、多表连接就会顺畅得多。